diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b80da1c..e840456 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: # mix release # TODO: Upload build artifacts if needed - # - uses: actions/upload-artifact@v4 + # - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 # with: # name: release-artifacts # path: target/release/ @@ -94,7 +94,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # TODO: Download build artifacts if uploading to the release - # - uses: actions/download-artifact@v4 + # - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 # with: # name: release-artifacts # path: artifacts/ diff --git a/tests/validate.test.affine b/tests/validate.test.affine new file mode 100644 index 0000000..cdc470d --- /dev/null +++ b/tests/validate.test.affine @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// Session Sentinel — Structural Validation Test Suite (AffineScript port). +// +// Step 2 of the estate-wide TypeScript → AffineScript migration campaign +// (hyperpolymath/standards#239 umbrella, #241 TAIL BATCH 1). +// +// This is a container/infrastructure repo with no compiled source code. +// CRG Grade C for this repo category means validating structural +// invariants: required files exist, configuration is syntactically valid, +// placeholders are resolved, security fields are present, and no secrets +// leak. +// +// Test categories (mirrored from validate.test.ts): +// UNIT — individual file existence checks +// SMOKE — basic content sanity (non-empty) +// P2P — property: all TOML files are readable + non-empty +// E2E — chain: discovery → content check → field validation +// CONTRACT — required fields in each config file +// ASPECT — no secrets or placeholders in config files +// BENCHMARK — directory scan timing +// +// Compared to the .ts source this port substitutes a regex/string-based +// field check for `parse as TOML` because AffineScript has no TOML +// parser binding today (gap noted in PR). Each contract assertion checks +// for the same `field = ` pattern the upstream test verified post-parse, +// so coverage is equivalent for the field-presence + placeholder-leak +// invariants this suite protects. + +use Deno::{ + readTextFile, walkRecursive, consoleError, + regexMatch, statSize, pathJoin, dateNow +}; + +// `string::ends_with` is not `pub` in the stdlib today, so inline a +// `string_sub`-backed equivalent (the merged check-ts-allowlist seed's +// defensive pattern, restored here). +fn ends_with(s: String, suffix: String) -> Bool { + let slen = len(s); + let sfxlen = len(suffix); + if sfxlen > slen { false } + else { string_sub(s, slen - sfxlen, sfxlen) == suffix } +} + +// ── Test infrastructure ───────────────────────────────────────────── +// +// Each `pub fn test_*() -> Bool` is exported so a future harness (the +// affinescript-deno-test convention) can wrap it as `Deno.test()`. For +// this port we additionally provide a `pub fn main()` which iterates +// the test list, reports per-test outcome, and exits non-zero on any +// failure — matching the shape `deno test` would produce while keeping +// the file directly runnable via `deno run --allow-read`. + +// Repository root, resolved as the parent of this test file's directory. +// `tests/` lives one level under the repo root. +fn repo_root() -> String { + // The compiled .deno.js runs from the repo root via `deno test` / + // `deno run tests/validate.test.deno.js`. Both invocations set the + // cwd to the repo root, so a "." prefix resolves to the repo. + "." +} + +// ── helpers ──────────────────────────────────────────────────────── + +fn join2(a: String, b: String) -> String { + pathJoin(a, b) +} + +fn join3(a: String, b: String, c: String) -> String { + pathJoin(pathJoin(a, b), c) +} + +// File-exists check: `Deno.statSync(path).size` throws on missing file; +// the call returns >= 0 when the entry exists and is reachable. We pair +// this with a `readTextFile` for the content-bearing assertions because +// a directory's `.size` is platform-specific. +fn file_exists(path: String) -> Bool { + let _ = statSize(path); + true +} + +fn file_non_empty(path: String) -> Bool { + let content = readTextFile(path); + len(content) > 0 +} + +fn read_file(path: String) -> String { + readTextFile(path) +} + +// Substring containment via the host `String.indexOf` (>= 0 = found). +fn contains(haystack: String, needle: String) -> Bool { + string_find(haystack, needle) >= 0 +} + +// `String.includes`-shaped helper used by the contract / aspect arms. +fn includes(s: String, sub: String) -> Bool { + contains(s, sub) +} + +// ── UNIT: required files exist ────────────────────────────────────── + +pub fn test_unit_readme_exists() -> Bool { + file_exists(join2(repo_root(), "README.adoc")) +} + +pub fn test_unit_license_exists() -> Bool { + file_exists(join2(repo_root(), "LICENSE")) +} + +pub fn test_unit_containerfile_exists() -> Bool { + file_exists(join2(repo_root(), "Containerfile")) +} + +pub fn test_unit_sentinel_config_exists() -> Bool { + file_exists(join3(repo_root(), "config", "session-sentinel.toml")) +} + +pub fn test_unit_manifest_exists() -> Bool { + file_exists(join3(repo_root(), "container", "manifest.toml")) +} + +pub fn test_unit_security_txt_exists() -> Bool { + file_exists(join3(repo_root(), ".well-known", "security.txt")) +} + +// ── UNIT: required directories exist ──────────────────────────────── +// +// AffineScript's Deno bindings expose `statSize` (file bytes); a +// directory call succeeds on every supported platform — its size is +// platform-defined but the call itself does not throw, which is the +// existence signal we need. We also assert that the directory contains +// at least one expected child so a stray file with the same name +// doesn't satisfy the test. + +pub fn test_unit_config_dir_exists() -> Bool { + let _ = file_exists(join2(repo_root(), "config")); + file_exists(join3(repo_root(), "config", "session-sentinel.toml")) +} + +pub fn test_unit_container_dir_exists() -> Bool { + let _ = file_exists(join2(repo_root(), "container")); + file_exists(join3(repo_root(), "container", "manifest.toml")) +} + +pub fn test_unit_docs_dir_exists() -> Bool { + file_exists(join2(repo_root(), "docs")) +} + +// ── SMOKE: files have non-zero content ────────────────────────────── + +pub fn test_smoke_readme_nonempty() -> Bool { + file_non_empty(join2(repo_root(), "README.adoc")) +} + +pub fn test_smoke_license_nonempty() -> Bool { + file_non_empty(join2(repo_root(), "LICENSE")) +} + +pub fn test_smoke_sentinel_config_nonempty() -> Bool { + file_non_empty(join3(repo_root(), "config", "session-sentinel.toml")) +} + +// ── P2P: every .toml file is readable and non-empty ───────────────── +// +// AffineScript has no `parse as TOML` extern, so this property +// substitutes a "readable + non-empty + recognisable shape" check. +// Every TOML file must contain at least one `[section]` header or +// `key =` assignment line, which excludes accidental empty/binary +// drops without depending on a full parser. + +fn looks_like_toml(content: String) -> Bool { + // Either a section header `[name]` or an assignment `key = value`. + regexMatch(content, "(^|\\n)\\s*\\[[A-Za-z_][A-Za-z0-9_.-]*\\]") || + regexMatch(content, "(^|\\n)\\s*[A-Za-z_][A-Za-z0-9_-]*\\s*=") +} + +fn collect_toml_files(root: String) -> [String] { + let mut out = []; + let all = walkRecursive(root); + let mut i = 0; + let n = len(all); + while i < n { + let f = all[i]; + if ends_with(f, ".toml") { + // Skip anything under a `.git*` segment so the .git index TOML + // pseudo-files (or vendored archives) don't pollute the run. + let mut skip = false; + if regexMatch(f, "/\\.git") { skip = true; } + if !skip { out = out ++ [f]; } + } + i = i + 1; + } + out +} + +pub fn test_p2p_all_toml_files_well_shaped() -> Bool { + let files = collect_toml_files(repo_root()); + if len(files) == 0 { return false; } + let mut i = 0; + let n = len(files); + while i < n { + let f = files[i]; + let content = readTextFile(f); + if len(content) == 0 { return false; } + if !looks_like_toml(content) { + let _ = consoleError("p2p: malformed TOML shape: " ++ f); + return false; + } + i = i + 1; + } + true +} + +// ── E2E: file discovery → content read → field validation ─────────── + +pub fn test_e2e_sentinel_config_chain() -> Bool { + // Stage 1: discoverable + let path = join3(repo_root(), "config", "session-sentinel.toml"); + if !file_exists(path) { return false; } + // Stage 2: readable + let content = read_file(path); + if len(content) == 0 { return false; } + // Stage 3: TOML-shaped (top-level [sentinel] section) + if !regexMatch(content, "(^|\\n)\\s*\\[sentinel\\]") { return false; } + // Stage 4: contract field present + if !regexMatch(content, "(^|\\n)\\s*scan_interval\\s*=") { return false; } + true +} + +pub fn test_e2e_manifest_chain() -> Bool { + // Stage 1: discoverable + let path = join3(repo_root(), "container", "manifest.toml"); + if !file_exists(path) { return false; } + // Stage 2: readable + let content = read_file(path); + if len(content) == 0 { return false; } + // Stage 3: required [metadata] section + fields + if !regexMatch(content, "(^|\\n)\\s*\\[metadata\\]") { return false; } + if !regexMatch(content, "(^|\\n)\\s*name\\s*=") { return false; } + if !regexMatch(content, "(^|\\n)\\s*version\\s*=") { return false; } + if !regexMatch(content, "(^|\\n)\\s*license\\s*=") { return false; } + // Stage 4: license correctness — must be MPL-2.0 + regexMatch(content, "license\\s*=\\s*\"MPL-2\\.0\"") +} + +// ── CONTRACT: required fields in each config file ────────────────── + +pub fn test_contract_sentinel_required_fields() -> Bool { + let content = read_file(join3(repo_root(), "config", "session-sentinel.toml")); + if !regexMatch(content, "(^|\\n)\\s*\\[sentinel\\]") { return false; } + if !regexMatch(content, "(^|\\n)\\s*scan_interval\\s*=") { return false; } + if !regexMatch(content, "(^|\\n)\\s*enable_self_healing\\s*=") { return false; } + regexMatch(content, "(^|\\n)\\s*log_path\\s*=") +} + +pub fn test_contract_security_txt_rfc9116_fields() -> Bool { + let content = read_file(join3(repo_root(), ".well-known", "security.txt")); + if !includes(content, "Contact:") { return false; } + if !includes(content, "Expires:") { return false; } + includes(content, "Preferred-Languages:") +} + +pub fn test_contract_manifest_security_section() -> Bool { + let content = read_file(join3(repo_root(), "container", "manifest.toml")); + if !regexMatch(content, "(^|\\n)\\s*\\[security\\]") { return false; } + if !regexMatch(content, "(^|\\n)\\s*user\\s*=") { return false; } + regexMatch(content, "(^|\\n)\\s*no_new_privileges\\s*=") +} + +// ── ASPECT: no secrets / placeholders in config files ────────────── +// +// Scans for secret-shaped patterns. Mirrors the TS predicate set but +// implemented as case-insensitive AffineScript `regexMatch`. The +// emitted JS uses `new RegExp(pat).test(str)`, so JS-flavour regex +// syntax (`[a-z]`, alternation, anchors, `\s`, `\d`) is portable. We +// embed `(?i)` per-pattern by listing both case variants in a single +// alternation rather than relying on inline flags, which is portable +// across regex flavours. + +fn contains_secret_pattern(content: String) -> Bool { + // api_key / apikey / api-key = "<>= 8 chars>" + if regexMatch(content, "[Aa][Pp][Ii][-_]?[Kk][Ee][Yy]\\s*=\\s*[\"'][^\"']{8,}[\"']") { + return true; + } + // password / passwd / pwd = "<>= 4 chars>" + if regexMatch(content, "(password|passwd|pwd|PASSWORD|PASSWD|PWD)\\s*=\\s*[\"'][^\"']{4,}[\"']") { + return true; + } + // secret / token = "" + if regexMatch(content, "(secret|token|SECRET|TOKEN)\\s*=\\s*[\"'][A-Za-z0-9+/]{20,}[\"']") { + return true; + } + // PEM private key block + if regexMatch(content, "-----BEGIN (RSA |EC )?PRIVATE KEY-----") { + return true; + } + // Cloud-provider env-style secrets + if regexMatch(content, "(AWS|AZURE|GCP)_(SECRET|KEY|TOKEN)\\s*=") { + return true; + } + // Long hex secrets (32+ chars) keyed by secret/key/token + if regexMatch(content, "(secret|key|token|SECRET|KEY|TOKEN)\\s*=\\s*[\"'][0-9a-fA-F]{32,}[\"']") { + return true; + } + false +} + +pub fn test_aspect_no_secrets_in_sentinel_toml() -> Bool { + let content = read_file(join3(repo_root(), "config", "session-sentinel.toml")); + !contains_secret_pattern(content) +} + +pub fn test_aspect_no_secrets_in_container_tomls() -> Bool { + let container_dir = join2(repo_root(), "container"); + let all = walkRecursive(container_dir); + let mut i = 0; + let n = len(all); + while i < n { + let f = all[i]; + if ends_with(f, ".toml") { + let content = readTextFile(f); + if contains_secret_pattern(content) { + let _ = consoleError("aspect: secret pattern in " ++ f); + return false; + } + } + i = i + 1; + } + true +} + +pub fn test_aspect_critical_fields_not_placeholders() -> Bool { + let content = read_file(join3(repo_root(), "container", "manifest.toml")); + // name / version / license must not be raw `{{...}}` placeholders. + if regexMatch(content, "(^|\\n)\\s*name\\s*=\\s*[\"'][^\"']*\\{\\{") { return false; } + if regexMatch(content, "(^|\\n)\\s*version\\s*=\\s*[\"'][^\"']*\\{\\{") { return false; } + if regexMatch(content, "(^|\\n)\\s*license\\s*=\\s*[\"'][^\"']*\\{\\{") { return false; } + true +} + +// ── BENCHMARK: full repo TOML scan ────────────────────────────────── +// +// AffineScript's only host-clock extern on the Deno-ESM backend is +// `dateNow()` (millis-since-epoch as Int) — fine-grained enough for a +// 2s budget. We require both that the scan completes under budget AND +// that it found at least one TOML file (so a silently-broken +// walkRecursive doesn't trivially "pass"). + +pub fn test_benchmark_toml_scan_under_2s() -> Bool { + let start = dateNow(); + let files = collect_toml_files(repo_root()); + let mut i = 0; + let n = len(files); + while i < n { + let _ = readTextFile(files[i]); + i = i + 1; + } + let elapsed = dateNow() - start; + if len(files) == 0 { return false; } + let _ = consoleError( + " benchmark: scanned " ++ int_to_string(len(files)) ++ + " TOML files in " ++ int_to_string(elapsed) ++ "ms" + ); + elapsed < 2000 +} + +// ── Driver ────────────────────────────────────────────────────────── +// +// Iterates the named test list, prints a tap-ish per-test line, and +// exits 0 on all-pass / 1 on any-fail. Each test runs inside its own +// helper invocation; an uncaught `panic`/throw from `statSize` on a +// missing file bubbles to the runtime as a failed test (the wrapping +// `try`/`catch` would need an effect-tracked extern surface we do not +// yet have on this backend — documented as a follow-up). + +fn run_one(name: String, ok: Bool) -> Int { + if ok { + println(" ok " ++ name); + return 0; + } + println(" FAIL " ++ name); + return 1; +} + +pub fn main() -> Int { + let mut failed = 0; + failed = failed + run_one("unit: README.adoc exists", test_unit_readme_exists()); + failed = failed + run_one("unit: LICENSE exists", test_unit_license_exists()); + failed = failed + run_one("unit: Containerfile exists", test_unit_containerfile_exists()); + failed = failed + run_one("unit: config/session-sentinel.toml exists", test_unit_sentinel_config_exists()); + failed = failed + run_one("unit: container/manifest.toml exists", test_unit_manifest_exists()); + failed = failed + run_one("unit: .well-known/security.txt exists", test_unit_security_txt_exists()); + failed = failed + run_one("unit: config/ directory exists", test_unit_config_dir_exists()); + failed = failed + run_one("unit: container/ directory exists", test_unit_container_dir_exists()); + failed = failed + run_one("unit: docs/ directory exists", test_unit_docs_dir_exists()); + + failed = failed + run_one("smoke: README.adoc is non-empty", test_smoke_readme_nonempty()); + failed = failed + run_one("smoke: LICENSE is non-empty", test_smoke_license_nonempty()); + failed = failed + run_one("smoke: config/session-sentinel.toml is non-empty", test_smoke_sentinel_config_nonempty()); + + failed = failed + run_one("p2p: all TOML files are well-shaped", test_p2p_all_toml_files_well_shaped()); + + failed = failed + run_one("e2e: session-sentinel.toml chain", test_e2e_sentinel_config_chain()); + failed = failed + run_one("e2e: container manifest chain", test_e2e_manifest_chain()); + + failed = failed + run_one("contract: sentinel required fields", test_contract_sentinel_required_fields()); + failed = failed + run_one("contract: security.txt RFC 9116 fields", test_contract_security_txt_rfc9116_fields()); + failed = failed + run_one("contract: manifest security section", test_contract_manifest_security_section()); + + failed = failed + run_one("aspect: no secrets in sentinel toml", test_aspect_no_secrets_in_sentinel_toml()); + failed = failed + run_one("aspect: no secrets in container tomls", test_aspect_no_secrets_in_container_tomls()); + failed = failed + run_one("aspect: critical fields not placeholders", test_aspect_critical_fields_not_placeholders()); + + failed = failed + run_one("benchmark: full repo TOML scan < 2s", test_benchmark_toml_scan_under_2s()); + + if failed > 0 { + println(""); + println("FAILED: " ++ int_to_string(failed) ++ " test(s)"); + return 1; + } + println(""); + println("OK: all tests passed"); + return 0; +} diff --git a/tests/validate.test.deno.js b/tests/validate.test.deno.js new file mode 100644 index 0000000..46aec09 --- /dev/null +++ b/tests/validate.test.deno.js @@ -0,0 +1,485 @@ +// Generated by AffineScript compiler (Deno-ESM target, issue #122) +// SPDX-License-Identifier: MPL-2.0 +// ---- AffineScript Deno-ESM runtime ---- +const Some = (value) => ({ tag: "Some", value }); +const None = { tag: "None" }; +const Ok = (value) => ({ tag: "Ok", value }); +const Err = (error) => ({ tag: "Err", error }); +const Unit = null; +const print = (s) => { Deno.stdout.writeSync(new TextEncoder().encode(String(s))); }; +const println = (s) => { console.log(String(s)); }; +// ---- Deno host shims (extern fn lowering targets, issue #122) ---- +// Kept tiny + inlined so emitted modules are genuinely drop-in (no extra +// package to publish or resolve). The same surface is mirrored, for +// standalone `deno test`, by packages/affine-deno/mod.js. +const __as_ensureDir = (p) => { + try { Deno.mkdirSync(p, { recursive: true }); } + catch (e) { if (!(e instanceof Deno.errors.AlreadyExists)) throw e; } +}; +const __as_pathJoin = (a, b) => { + if (a.length === 0) return b; + const sep = a.endsWith("/") || a.endsWith("\\") ? "" : "/"; + return a + sep + b; +}; +const __as_readDirNames = (p) => { + const names = []; + for (const entry of Deno.readDirSync(p)) { + if (entry.isFile) names.push(entry.name); + } + return names; +}; +const __as_isNotFound = (e) => (e instanceof Deno.errors.NotFound); +const __as_walkRecursive = (root) => { + const out = []; + const rec = (dir) => { + for (const entry of Deno.readDirSync(dir)) { + const full = (dir.endsWith("/") ? dir : dir + "/") + entry.name; + if (entry.isFile) out.push(full); + else if (entry.isDirectory) rec(full); + } + }; + rec(root); + return out; +}; +const __as_regexMatch = (s, pat) => new RegExp(pat).test(String(s)); +const __as_wasmInstance = (bytes) => + new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports; +const __as_wasmCall = (exports, name, args) => Number(exports[name](...(args || []))); +// ---- motion (bindings #4): consumer-provided import ---- +// Host JS environment must expose globalThis.__as_motion (the motion +// library or a compatible mock). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import * as m from "motion"; globalThis.__as_motion = m;` once at +// module-init time. The AffineScript-side externs (stdlib/Motion.affine) +// don't see this indirection — they call __as_motion* helpers directly. +const __as_motionAnimate = (target, keyframes, options) => + globalThis.__as_motion.animate(target, keyframes, options); +const __as_motionAwait = (controls) => + Promise.resolve(controls).then(() => 0); +const __as_motionCancel = (controls) => { + if (controls && typeof controls.cancel === "function") controls.cancel(); + return 0; +}; +// `animateMini` / `tween` / `spring` / `ease` — bindings #4 follow-up +// surface. Each helper resolves the host method on globalThis.__as_motion +// at call time so a mock that only stubs a subset still works for the +// rest (the smoke harness exercises every variant). +const __as_motionAnimateMini = (target, keyframes, options) => + globalThis.__as_motion.animateMini(target, keyframes, options); +const __as_motionTween = (target, from, to, options) => + globalThis.__as_motion.tween(target, from, to, options); +const __as_motionSpring = (target, keyframes, springConfig) => + globalThis.__as_motion.spring(target, keyframes, springConfig); +const __as_motionEase = (name) => + globalThis.__as_motion.ease(name); +// ---- pixi.js (bindings #1): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi (the PIXI namespace +// from `import * as PIXI from "pixi.js"`). Tests set it in the harness +// before importing the generated module. +const __as_pixiAppInit = async (options) => { + const app = new globalThis.__as_pixi.Application(); + await app.init(options); + return app; +}; +const __as_pixiAppCanvas = (app) => app.canvas; +const __as_pixiAppStage = (app) => app.stage; +const __as_pixiAppTicker = (app) => app.ticker; +const __as_pixiAppDestroy = (app) => { app.destroy(); return 0; }; +const __as_pixiContainerNew = () => new globalThis.__as_pixi.Container(); +const __as_pixiContainerAddChild = (p, c) => { p.addChild(c); return 0; }; +const __as_pixiContainerRemoveChild = (p, c) => { p.removeChild(c); return 0; }; +const __as_pixiContainerSetPosition = (c, x, y) => { c.x = x; c.y = y; return 0; }; +const __as_pixiContainerSetVisible = (c, v) => { c.visible = v; return 0; }; +const __as_pixiContainerDestroy = (c) => { c.destroy(); return 0; }; +const __as_pixiSpriteFrom = (t) => new globalThis.__as_pixi.Sprite(t); +// Upcasts are identity — PIXI's class hierarchy makes Sprite/Graphics/ +// Text actual Container subclasses, so the JS object is the same. +const __as_pixiSpriteAsContainer = (s) => s; +const __as_pixiTextureFromUrl = (url) => globalThis.__as_pixi.Texture.from(url); +const __as_pixiGraphicsNew = () => new globalThis.__as_pixi.Graphics(); +const __as_pixiGraphicsRect = (g, x, y, w, h) => { g.rect(x, y, w, h); return 0; }; +const __as_pixiGraphicsFill = (g, color) => { g.fill({ color }); return 0; }; +const __as_pixiGraphicsClear = (g) => { g.clear(); return 0; }; +const __as_pixiGraphicsAsContainer = (g) => g; +const __as_pixiTextNew = (options) => new globalThis.__as_pixi.Text(options); +const __as_pixiTextSetText = (t, content) => { t.text = content; return 0; }; +const __as_pixiTextAsContainer = (t) => t; +const __as_pixiTickerAdd = (t, cb) => { t.add(cb); return 0; }; +const __as_pixiTickerStart = (t) => { t.start(); return 0; }; +const __as_pixiTickerStop = (t) => { t.stop(); return 0; }; +// ---- @pixi/ui (bindings #3): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_ui (the namespace +// from `import * as PixiUI from "@pixi/ui"`). Tests set it in the +// harness before importing the generated module; production +// consumers typically do once at module-init time. The +// AffineScript-side externs (stdlib/PixiUI.affine) don't see this +// indirection — they call __as_pixiUi* helpers directly. +// +// Upcasts to Container are identity — @pixi/ui's Button / +// FancyButton / Slider / Switch are all real PIXI.Container +// subclasses, so the JS object is the same. +const __as_pixiUiButtonNew = (options) => new globalThis.__as_pixi_ui.Button(options); +const __as_pixiUiButtonOnPress = (b, cb) => { b.onPress.connect(cb); return 0; }; +const __as_pixiUiButtonAsContainer = (b) => b; +const __as_pixiUiFancyButtonNew = (options) => new globalThis.__as_pixi_ui.FancyButton(options); +const __as_pixiUiFancyButtonAsContainer = (b) => b; +const __as_pixiUiSliderNew = (options) => new globalThis.__as_pixi_ui.Slider(options); +const __as_pixiUiSliderOnUpdate = (s, cb) => { s.onUpdate.connect(cb); return 0; }; +const __as_pixiUiSliderAsContainer = (s) => s; +const __as_pixiUiSwitchNew = (options) => new globalThis.__as_pixi_ui.Switch(options); +const __as_pixiUiSwitchOnChange = (sw, cb) => { sw.onChange.connect(cb); return 0; }; +const __as_pixiUiSwitchAsContainer = (sw) => sw; +// ---- @pixi/sound (bindings #2): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_sound (the `Sound` +// named export from `@pixi/sound`). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import { Sound } from "@pixi/sound"; globalThis.__as_pixi_sound = Sound;` +// once at module-init time. The AffineScript-side externs +// (stdlib/PixiSound.affine) don't see this indirection — they call +// __as_pixiSound* helpers directly. +const __as_pixiSoundFrom = (url) => globalThis.__as_pixi_sound.from(url); +const __as_pixiSoundPlay = (s) => { s.play(); return 0; }; +const __as_pixiSoundStop = (s) => { s.stop(); return 0; }; +const __as_pixiSoundPause = (s) => { s.pause(); return 0; }; +const __as_pixiSoundResume = (s) => { s.resume(); return 0; }; +const __as_pixiSoundSetVolume = (s, vol) => { s.volume = vol; return 0; }; +const __as_pixiSoundSetLoop = (s, loop) => { s.loop = loop; return 0; }; +// `++` is overloaded (string concat / array concat); `a + b` would +// stringify arrays. Dispatch on shape so stdlib/string.affine's +// `result ++ [x]` and `a ++ b` are both correct. +const __as_concat = (a, b) => Array.isArray(a) ? a.concat(b) : (a + b); +// Honest host/runtime primitives underpinning the AffineScript-level +// stdlib/string.affine (its is_empty/starts_with/ends_with/split/join/ +// replace/... are real AffineScript on top of these). +const __as_strSub = (s, start, n) => String(s).slice(start, start + n); +const __as_strGet = (s, i) => String(s)[i]; +const __as_strFind = (s, n) => String(s).indexOf(n); +const __as_charToInt = (c) => String(c).codePointAt(0); +const __as_intToChar = (n) => String.fromCodePoint(n); +const __as_parseInt = (s) => { + const n = parseInt(String(s), 10); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_parseFloat = (s) => { + const n = parseFloat(String(s)); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_show = (v) => (typeof v === "string" ? v : JSON.stringify(v)); +// ---- Http (issue #160): portable fetch round-trip ---- +// `headers` crosses the boundary as an AffineScript [(String, String)] +// assoc list == JS array of [name, value] pairs. `body` is an +// AffineScript Option == { tag: "Some", value } | { tag: "None" }. +// The result is the `Response` record shape { status, headers, body }. +const __as_httpHeadersToObject = (pairs) => { + const o = {}; + for (const kv of (pairs || [])) o[kv[0]] = kv[1]; + return o; +}; +const __as_httpHeadersFromResponse = (res) => { + const out = []; + res.headers.forEach((value, key) => out.push([key, value])); + return out; +}; +// ---- hpm-json-rsr Zig FFI shims (stdlib/json.affine v0.3) ---- +// `HpmJsonValue` is opaque to AffineScript; on Deno-ESM it's just the +// underlying JS value from JSON.parse. The shims mirror the sentinel +// conventions of the Zig exports so the AffineScript-side wrappers +// (`to_json`, `parse`) behave identically across backends. +const __as_hpmJsonParse = (s) => { + try { return Some(JSON.parse(String(s))); } catch (_e) { return None; } +}; +const __as_hpmJsonFree = (_v) => 0; +const __as_hpmJsonType = (v) => { + if (v === null || v === undefined) return 0; + if (typeof v === "boolean") return 1; + if (typeof v === "number") return Number.isInteger(v) ? 2 : 3; + if (typeof v === "string") return 4; + if (Array.isArray(v)) return 5; + if (typeof v === "object") return 6; + return -1; +}; +const __as_hpmJsonBool = (v) => (typeof v === "boolean" ? (v ? 1 : 0) : -1); +const __as_hpmJsonInt = (v) => + (typeof v === "number" ? Math.trunc(v) : Number.MIN_SAFE_INTEGER); +const __as_hpmJsonFloat = (v) => (typeof v === "number" ? v : NaN); +const __as_hpmJsonString = (v) => (typeof v === "string" ? v : ""); +const __as_hpmJsonObjectGet = (v, k) => { + if (v === null || typeof v !== "object" || Array.isArray(v)) return None; + return Object.prototype.hasOwnProperty.call(v, String(k)) + ? Some(v[String(k)]) : None; +}; +const __as_hpmJsonArrayLen = (v) => (Array.isArray(v) ? v.length : 0); +const __as_hpmJsonArrayGet = (v, i) => { + if (!Array.isArray(v)) return None; + const idx = Number(i); + return (idx >= 0 && idx < v.length) ? Some(v[idx]) : None; +}; +const __as_hpmJsonEscapeString = (s) => { + let out = ""; + const src = String(s); + for (let i = 0; i < src.length; i++) { + const c = src.charCodeAt(i); + if (c === 0x22) out += "\\\""; + else if (c === 0x5c) out += "\\\\"; + else if (c === 0x0a) out += "\\n"; + else if (c === 0x0d) out += "\\r"; + else if (c === 0x09) out += "\\t"; + else if (c === 0x08) out += "\\b"; + else if (c === 0x0c) out += "\\f"; + else if (c < 0x20) out += "\\u00" + c.toString(16).padStart(2, "0"); + else out += src[i]; + } + return out; +}; +const __as_httpFetch = async (url, method, headers, bodyOpt) => { + const init = { method, headers: __as_httpHeadersToObject(headers) }; + if (bodyOpt && bodyOpt.tag === "Some") init.body = bodyOpt.value; + // `globalThis.fetch` explicitly: the stdlib `Http.fetch` compiles to a + // module-level `function fetch`, which would otherwise shadow the host. + const res = await globalThis.fetch(url, init); + const text = await res.text(); + return { + status: res.status, + headers: __as_httpHeadersFromResponse(res), + body: text, + }; +}; +// ---- end runtime ---- + +function ends_with(s, suffix) { + const slen = ((s).length); + const sfxlen = ((suffix).length); + return ((sfxlen > slen) ? (() => { return false; })() : (() => { return (__as_strSub(s, (slen - sfxlen), sfxlen) === suffix); })()); +} + +function repo_root() { + return "."; +} + +function join2(a, b) { + return __as_pathJoin(a, b); +} + +function join3(a, b, c) { + return __as_pathJoin(__as_pathJoin(a, b), c); +} + +function file_exists(path) { + const _ = Deno.statSync(path).size; + return true; +} + +function file_non_empty(path) { + const content = Deno.readTextFileSync(path); + return (((content).length) > 0); +} + +function read_file(path) { + return Deno.readTextFileSync(path); +} + +function contains(haystack, needle) { + return (__as_strFind(haystack, needle) >= 0); +} + +function includes(s, sub) { + return contains(s, sub); +} + +export function test_unit_readme_exists() { + return file_exists(join2(repo_root(), "README.adoc")); +} + +export function test_unit_license_exists() { + return file_exists(join2(repo_root(), "LICENSE")); +} + +export function test_unit_containerfile_exists() { + return file_exists(join2(repo_root(), "Containerfile")); +} + +export function test_unit_sentinel_config_exists() { + return file_exists(join3(repo_root(), "config", "session-sentinel.toml")); +} + +export function test_unit_manifest_exists() { + return file_exists(join3(repo_root(), "container", "manifest.toml")); +} + +export function test_unit_security_txt_exists() { + return file_exists(join3(repo_root(), ".well-known", "security.txt")); +} + +export function test_unit_config_dir_exists() { + const _ = file_exists(join2(repo_root(), "config")); + return file_exists(join3(repo_root(), "config", "session-sentinel.toml")); +} + +export function test_unit_container_dir_exists() { + const _ = file_exists(join2(repo_root(), "container")); + return file_exists(join3(repo_root(), "container", "manifest.toml")); +} + +export function test_unit_docs_dir_exists() { + return file_exists(join2(repo_root(), "docs")); +} + +export function test_smoke_readme_nonempty() { + return file_non_empty(join2(repo_root(), "README.adoc")); +} + +export function test_smoke_license_nonempty() { + return file_non_empty(join2(repo_root(), "LICENSE")); +} + +export function test_smoke_sentinel_config_nonempty() { + return file_non_empty(join3(repo_root(), "config", "session-sentinel.toml")); +} + +function looks_like_toml(content) { + return (__as_regexMatch(content, "(^|\\n)\\s*\\[[A-Za-z_][A-Za-z0-9_.-]*\\]") || __as_regexMatch(content, "(^|\\n)\\s*[A-Za-z_][A-Za-z0-9_-]*\\s*=")); +} + +function collect_toml_files(root) { + let out = []; + const all = __as_walkRecursive(root); + let i = 0; + const n = ((all).length); + while ((i < n)) { const f = all[i]; if (ends_with(f, ".toml")) { let skip = false; if (__as_regexMatch(f, "/\\.git")) { skip = true; } if ((!skip)) { out = __as_concat(out, [f]); } } i = (i + 1); } + return out; +} + +export function test_p2p_all_toml_files_well_shaped() { + const files = collect_toml_files(repo_root()); + if ((((files).length) === 0)) { return false; } + let i = 0; + const n = ((files).length); + while ((i < n)) { const f = files[i]; const content = Deno.readTextFileSync(f); if ((((content).length) === 0)) { return false; } if ((!looks_like_toml(content))) { const _ = (console.error(__as_concat("p2p: malformed TOML shape: ", f)), 0); return false; } i = (i + 1); } + return true; +} + +export function test_e2e_sentinel_config_chain() { + const path = join3(repo_root(), "config", "session-sentinel.toml"); + if ((!file_exists(path))) { return false; } + const content = read_file(path); + if ((((content).length) === 0)) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*\\[sentinel\\]"))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*scan_interval\\s*="))) { return false; } + return true; +} + +export function test_e2e_manifest_chain() { + const path = join3(repo_root(), "container", "manifest.toml"); + if ((!file_exists(path))) { return false; } + const content = read_file(path); + if ((((content).length) === 0)) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*\\[metadata\\]"))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*name\\s*="))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*version\\s*="))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*license\\s*="))) { return false; } + return __as_regexMatch(content, "license\\s*=\\s*\"MPL-2\\.0\""); +} + +export function test_contract_sentinel_required_fields() { + const content = read_file(join3(repo_root(), "config", "session-sentinel.toml")); + if ((!__as_regexMatch(content, "(^|\\n)\\s*\\[sentinel\\]"))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*scan_interval\\s*="))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*enable_self_healing\\s*="))) { return false; } + return __as_regexMatch(content, "(^|\\n)\\s*log_path\\s*="); +} + +export function test_contract_security_txt_rfc9116_fields() { + const content = read_file(join3(repo_root(), ".well-known", "security.txt")); + if ((!includes(content, "Contact:"))) { return false; } + if ((!includes(content, "Expires:"))) { return false; } + return includes(content, "Preferred-Languages:"); +} + +export function test_contract_manifest_security_section() { + const content = read_file(join3(repo_root(), "container", "manifest.toml")); + if ((!__as_regexMatch(content, "(^|\\n)\\s*\\[security\\]"))) { return false; } + if ((!__as_regexMatch(content, "(^|\\n)\\s*user\\s*="))) { return false; } + return __as_regexMatch(content, "(^|\\n)\\s*no_new_privileges\\s*="); +} + +function contains_secret_pattern(content) { + if (__as_regexMatch(content, "[Aa][Pp][Ii][-_]?[Kk][Ee][Yy]\\s*=\\s*[\"'][^\"']{8,}[\"']")) { return true; } + if (__as_regexMatch(content, "(password|passwd|pwd|PASSWORD|PASSWD|PWD)\\s*=\\s*[\"'][^\"']{4,}[\"']")) { return true; } + if (__as_regexMatch(content, "(secret|token|SECRET|TOKEN)\\s*=\\s*[\"'][A-Za-z0-9+/]{20,}[\"']")) { return true; } + if (__as_regexMatch(content, "-----BEGIN (RSA |EC )?PRIVATE KEY-----")) { return true; } + if (__as_regexMatch(content, "(AWS|AZURE|GCP)_(SECRET|KEY|TOKEN)\\s*=")) { return true; } + if (__as_regexMatch(content, "(secret|key|token|SECRET|KEY|TOKEN)\\s*=\\s*[\"'][0-9a-fA-F]{32,}[\"']")) { return true; } + return false; +} + +export function test_aspect_no_secrets_in_sentinel_toml() { + const content = read_file(join3(repo_root(), "config", "session-sentinel.toml")); + return (!contains_secret_pattern(content)); +} + +export function test_aspect_no_secrets_in_container_tomls() { + const container_dir = join2(repo_root(), "container"); + const all = __as_walkRecursive(container_dir); + let i = 0; + const n = ((all).length); + while ((i < n)) { const f = all[i]; if (ends_with(f, ".toml")) { const content = Deno.readTextFileSync(f); if (contains_secret_pattern(content)) { const _ = (console.error(__as_concat("aspect: secret pattern in ", f)), 0); return false; } } i = (i + 1); } + return true; +} + +export function test_aspect_critical_fields_not_placeholders() { + const content = read_file(join3(repo_root(), "container", "manifest.toml")); + if (__as_regexMatch(content, "(^|\\n)\\s*name\\s*=\\s*[\"'][^\"']*\\{\\{")) { return false; } + if (__as_regexMatch(content, "(^|\\n)\\s*version\\s*=\\s*[\"'][^\"']*\\{\\{")) { return false; } + if (__as_regexMatch(content, "(^|\\n)\\s*license\\s*=\\s*[\"'][^\"']*\\{\\{")) { return false; } + return true; +} + +export function test_benchmark_toml_scan_under_2s() { + const start = Date.now(); + const files = collect_toml_files(repo_root()); + let i = 0; + const n = ((files).length); + while ((i < n)) { const _ = Deno.readTextFileSync(files[i]); i = (i + 1); } + const elapsed = (Date.now() - start); + if ((((files).length) === 0)) { return false; } + const _ = (console.error(__as_concat(__as_concat(__as_concat(__as_concat(" benchmark: scanned ", String(((files).length))), " TOML files in "), String(elapsed)), "ms")), 0); + return (elapsed < 2000); +} + +function run_one(name, ok) { + if (ok) { println(__as_concat(" ok ", name)); return 0; } + println(__as_concat(" FAIL ", name)); + return 1; +} + +export function main() { + let failed = 0; + failed = (failed + run_one("unit: README.adoc exists", test_unit_readme_exists())); + failed = (failed + run_one("unit: LICENSE exists", test_unit_license_exists())); + failed = (failed + run_one("unit: Containerfile exists", test_unit_containerfile_exists())); + failed = (failed + run_one("unit: config/session-sentinel.toml exists", test_unit_sentinel_config_exists())); + failed = (failed + run_one("unit: container/manifest.toml exists", test_unit_manifest_exists())); + failed = (failed + run_one("unit: .well-known/security.txt exists", test_unit_security_txt_exists())); + failed = (failed + run_one("unit: config/ directory exists", test_unit_config_dir_exists())); + failed = (failed + run_one("unit: container/ directory exists", test_unit_container_dir_exists())); + failed = (failed + run_one("unit: docs/ directory exists", test_unit_docs_dir_exists())); + failed = (failed + run_one("smoke: README.adoc is non-empty", test_smoke_readme_nonempty())); + failed = (failed + run_one("smoke: LICENSE is non-empty", test_smoke_license_nonempty())); + failed = (failed + run_one("smoke: config/session-sentinel.toml is non-empty", test_smoke_sentinel_config_nonempty())); + failed = (failed + run_one("p2p: all TOML files are well-shaped", test_p2p_all_toml_files_well_shaped())); + failed = (failed + run_one("e2e: session-sentinel.toml chain", test_e2e_sentinel_config_chain())); + failed = (failed + run_one("e2e: container manifest chain", test_e2e_manifest_chain())); + failed = (failed + run_one("contract: sentinel required fields", test_contract_sentinel_required_fields())); + failed = (failed + run_one("contract: security.txt RFC 9116 fields", test_contract_security_txt_rfc9116_fields())); + failed = (failed + run_one("contract: manifest security section", test_contract_manifest_security_section())); + failed = (failed + run_one("aspect: no secrets in sentinel toml", test_aspect_no_secrets_in_sentinel_toml())); + failed = (failed + run_one("aspect: no secrets in container tomls", test_aspect_no_secrets_in_container_tomls())); + failed = (failed + run_one("aspect: critical fields not placeholders", test_aspect_critical_fields_not_placeholders())); + failed = (failed + run_one("benchmark: full repo TOML scan < 2s", test_benchmark_toml_scan_under_2s())); + if ((failed > 0)) { println(""); println(__as_concat(__as_concat("FAILED: ", String(failed)), " test(s)")); return 1; } + println(""); + println("OK: all tests passed"); + return 0; +} + +await main();