Skip to content

Commit cdf791f

Browse files
test(idris2): port 7 TS test files to Idris2 (estate port 11/11 — FINAL) (#56)
## Summary **Estate port 11/11 — final.** Converts the 7 TS test files in \`tests/{unit,contract,aspect,property,smoke,e2e,bench}_test.ts\` to Idris2 under the cladistic \`Test.Spec\` harness used by [ipfs-overlay](https://github.com/hyperpolymath/ipfs-overlay), [betlang](https://github.com/hyperpolymath/betlang), [stapeln](https://github.com/hyperpolymath/stapeln), [zerotier-k8s-link](https://github.com/hyperpolymath/zerotier-k8s-link), [thunderbird-template-reloaded](https://github.com/hyperpolymath/thunderbird-template-reloaded), etc. Originally tagged for bucket-2 (per-repo strategy review) — collapsed to bucket-1 (content-validation + pure-logic) after a single inspection. ## What ported | Module | Tests | Shape | |---|---:|---| | UnitTest | 20 | content-validation + pure-logic helpers | | ContractTest | 21 | content-validation | | AspectTest | 28 | content-validation (iterated TS loops expanded) | | PropertyTest | 16 | content-validation | | SmokeTest | 46 | content-validation | | E2ETest | 23 | content-validation | | BenchTest | 11 | functional pre-conditions only (no timing) | | **Total** | **165** | | ## Source bug fixed in this PR \`tests/property_test.ts:163-167\` iterates over \`contractileFiles = [Dustfile, Mustfile, Intentfile]\`. Only \`contractiles/dust/Dustfile\` and \`contractiles/must/Mustfile\` existed; **\`contractiles/lust/Intentfile\` was a missing scaffold companion** to the Dust (recovery) and Must (invariants) legs. The TS test would have failed against the prior repo state too. Fix: created \`contractiles/lust/Intentfile\` with the intent-contract template shape, mirroring the structure of its Mustfile sibling. **Same scaffold gap was observed and fixed in thunderbird-template-reloaded #63** — likely a template-leak from a shared scaffold generator. ## Test plan - [x] Local: \`idris2 --build universal-chat-extractor-tests.ipkg\` clean compile - [x] Local: \`./build/exec/universal-chat-extractor-tests\` — **165 / 165 PASS** - [ ] CI: dogfood-gate workflow picks up the new ipkg file ## Build gotchas pinned - \`mod n 60\` on \`Nat\` requires an \`Integral Nat\` instance that Prelude doesn't provide. Defined explicit \`modNat60\` that recurses by subtraction. - \`BenchTest\` needed \`import Data.Maybe\` for \`isJust\` (Prelude doesn't re-export it in 0.8.0). ## Estate-rollout status This PR completes **11/11 estate ports**. The bimodal rollout campaign closes here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6bb10f7 commit cdf791f

11 files changed

Lines changed: 1684 additions & 0 deletions

File tree

contractiles/lust/Intentfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# SPDX-License-Identifier: PLMP-1.0-or-later
2+
# Intentfile - declarative intent contract (template)
3+
# Sibling of contractiles/must/Mustfile (invariants) and
4+
# contractiles/dust/Dustfile (recovery). Captures the *desired*
5+
# end-state of the system separately from its invariants and
6+
# rollback hooks.
7+
8+
version: 1
9+
10+
metadata:
11+
name: project-intent-contract
12+
spec: v0.0.1
13+
description: "Declarative end-state intent for the project."
14+
15+
intents:
16+
- name: dependencies-resolved
17+
description: "All declared runtime dependencies must be locatable on PATH or via Guix."
18+
desired: "deno --version && pkg-config --version"
19+
rationale: "A missing dep at deploy time degrades to a non-recoverable error."
20+
21+
- name: scaffolding-complete
22+
description: "Templating scaffold renders without unresolved placeholders."
23+
desired: "grep -RE '\\{\\{\\s*[A-Z_]+\\s*\\}\\}' templates/ | wc -l | grep -q '^0$'"
24+
rationale: "Unrendered placeholders ship broken templates to downstream users."
25+
26+
- name: license-headers-present
27+
description: "Every source file carries an SPDX header."
28+
desired: "scripts/check-spdx.sh"
29+
rationale: "Estate-wide compliance posture (Hyperpolymath standard)."

tests/idris2/AspectTest.idr

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
--
4+
-- Port of tests/aspect_test.ts to Idris2, estate-rollout port 11/11.
5+
-- 21 of 21 cross-cutting aspect tests ported. Each iterated check (BANNED_FILES,
6+
-- DOCS, the tests/ directory walk) becomes one Idris2 test case to preserve
7+
-- granular per-name reporting.
8+
--
9+
-- The TS regex for secret-leak detection in README is replaced by literal
10+
-- substring checks against `api_key=`, `password=`, `secret=`, `token=`
11+
-- (case-insensitive); the equivalence is exact within ASCII.
12+
13+
module AspectTest
14+
15+
import Test.Spec
16+
import Data.String
17+
import System.File
18+
19+
%default covering
20+
21+
readFileToString : String -> IO String
22+
readFileToString path = do
23+
Right contents <- readFile path
24+
| Left _ => pure ""
25+
pure contents
26+
27+
fileExists : String -> IO Bool
28+
fileExists path = do
29+
Right _ <- readFile path
30+
| Left _ => pure False
31+
pure True
32+
33+
toLowerC : Char -> Char
34+
toLowerC c =
35+
if c >= 'A' && c <= 'Z'
36+
then chr (ord c + 32)
37+
else c
38+
39+
asciiLower : String -> String
40+
asciiLower s = pack (map toLowerC (unpack s))
41+
42+
||| True iff `content` (case-insensitively) contains any of the listed
43+
||| secret-leak patterns of the form `<name>=`.
44+
hasSecretLeak : String -> Bool
45+
hasSecretLeak content =
46+
let lc = asciiLower content in
47+
isInfixOf "api_key=" lc || isInfixOf "api_key =" lc ||
48+
isInfixOf "password=" lc || isInfixOf "password =" lc ||
49+
isInfixOf "secret=" lc || isInfixOf "secret =" lc ||
50+
isInfixOf "token=" lc || isInfixOf "token =" lc
51+
52+
public export
53+
allSuites : List TestCase
54+
allSuites =
55+
[ -- ---------- Security policy ----------
56+
57+
test "aspect/security: SECURITY.md exists" $ do
58+
ok <- fileExists "SECURITY.md"
59+
assertTrue "SECURITY.md present" ok
60+
61+
, test "aspect/security: SECURITY.md mentions vulnerability reporting" $ do
62+
content <- readFileToString "SECURITY.md"
63+
let lc = asciiLower content
64+
let hasDisclosure =
65+
isInfixOf "vulnerabilit" lc || isInfixOf "disclosure" lc ||
66+
isInfixOf "report" lc || isInfixOf "security" lc
67+
allPass
68+
[ assertTrue "SECURITY.md present" (content /= "")
69+
, assertTrue "SECURITY.md mentions security reporting" hasDisclosure
70+
]
71+
72+
, test "aspect/security: .well-known/security.txt exists" $ do
73+
ok <- fileExists ".well-known/security.txt"
74+
assertTrue ".well-known/security.txt present" ok
75+
76+
, test "aspect/security: no .env files in repo" $ do
77+
ok <- fileExists ".env"
78+
assertTrue ".env must not be committed" (not ok)
79+
80+
, test "aspect/security: no hardcoded secret patterns in README" $ do
81+
content <- readFileToString "README.adoc"
82+
allPass
83+
[ assertTrue "README.adoc present" (content /= "")
84+
, assertTrue "README.adoc has no hardcoded secrets"
85+
(not (hasSecretLeak content))
86+
]
87+
88+
-- ---------- Code of conduct ----------
89+
90+
, test "aspect/community: CODE_OF_CONDUCT.md exists" $ do
91+
ok <- fileExists "CODE_OF_CONDUCT.md"
92+
assertTrue "CODE_OF_CONDUCT.md present" ok
93+
94+
, test "aspect/community: CODE_OF_CONDUCT.md has meaningful content" $ do
95+
content <- readFileToString "CODE_OF_CONDUCT.md"
96+
assertTrue "CODE_OF_CONDUCT.md > 100 chars" (length content > 100)
97+
98+
-- ---------- EditorConfig consistency ----------
99+
100+
, test "aspect/formatting: .editorconfig exists" $ do
101+
ok <- fileExists ".editorconfig"
102+
assertTrue ".editorconfig present" ok
103+
104+
, test "aspect/formatting: .editorconfig has root = true" $ do
105+
content <- readFileToString ".editorconfig"
106+
let lc = asciiLower content
107+
allPass
108+
[ assertTrue ".editorconfig present" (content /= "")
109+
, assertTrue ".editorconfig declares root = true"
110+
(isInfixOf "root = true" lc || isInfixOf "root=true" lc)
111+
]
112+
113+
, test "aspect/formatting: .editorconfig defines indent_style" $ do
114+
content <- readFileToString ".editorconfig"
115+
assertTrue "indent_style present" (isInfixOf "indent_style" content)
116+
117+
-- ---------- No banned file patterns ----------
118+
-- TS iterates over a list; we expand to one test per name for clearer reports.
119+
120+
, test "aspect/policy: banned file must not exist - package.json" $ do
121+
ok <- fileExists "package.json"
122+
assertTrue "package.json must not exist" (not ok)
123+
124+
, test "aspect/policy: banned file must not exist - package-lock.json" $ do
125+
ok <- fileExists "package-lock.json"
126+
assertTrue "package-lock.json must not exist" (not ok)
127+
128+
, test "aspect/policy: banned file must not exist - yarn.lock" $ do
129+
ok <- fileExists "yarn.lock"
130+
assertTrue "yarn.lock must not exist" (not ok)
131+
132+
, test "aspect/policy: banned file must not exist - bun.lockb" $ do
133+
ok <- fileExists "bun.lockb"
134+
assertTrue "bun.lockb must not exist" (not ok)
135+
136+
, test "aspect/policy: banned file must not exist - node_modules" $ do
137+
ok <- fileExists "node_modules"
138+
assertTrue "node_modules must not exist" (not ok)
139+
140+
, test "aspect/policy: banned file must not exist - .npmrc" $ do
141+
ok <- fileExists ".npmrc"
142+
assertTrue ".npmrc must not exist" (not ok)
143+
144+
, test "aspect/policy: banned file must not exist - Dockerfile" $ do
145+
ok <- fileExists "Dockerfile"
146+
assertTrue "Dockerfile must not exist (use Containerfile)" (not ok)
147+
148+
-- ---------- No tsconfig.json ----------
149+
150+
, test "aspect/language: no tsconfig.json (TS only via Deno, not tsc)" $ do
151+
ok <- fileExists "tsconfig.json"
152+
assertTrue "tsconfig.json must not exist" (not ok)
153+
154+
-- ---------- Documentation completeness ----------
155+
156+
, test "aspect/docs: documentation file is non-empty - README.adoc" $ do
157+
content <- readFileToString "README.adoc"
158+
assertTrue "README.adoc > 50 chars" (length content > 50)
159+
160+
, test "aspect/docs: documentation file is non-empty - EXPLAINME.adoc" $ do
161+
content <- readFileToString "EXPLAINME.adoc"
162+
assertTrue "EXPLAINME.adoc > 50 chars" (length content > 50)
163+
164+
, test "aspect/docs: documentation file is non-empty - CONTRIBUTING.md" $ do
165+
content <- readFileToString "CONTRIBUTING.md"
166+
assertTrue "CONTRIBUTING.md > 50 chars" (length content > 50)
167+
168+
, test "aspect/docs: documentation file is non-empty - ROADMAP.adoc" $ do
169+
content <- readFileToString "ROADMAP.adoc"
170+
assertTrue "ROADMAP.adoc > 50 chars" (length content > 50)
171+
172+
-- ---------- All non-bench test files use Deno.test ----------
173+
-- The TS walks the tests/ dir; we enumerate explicitly. bench_test.ts is
174+
-- excluded per the TS predicate.
175+
176+
, test "aspect/tests: tests/unit_test.ts uses Deno.test" $ do
177+
content <- readFileToString "tests/unit_test.ts"
178+
assertTrue "Deno.test( present" (isInfixOf "Deno.test(" content)
179+
180+
, test "aspect/tests: tests/contract_test.ts uses Deno.test" $ do
181+
content <- readFileToString "tests/contract_test.ts"
182+
assertTrue "Deno.test( present" (isInfixOf "Deno.test(" content)
183+
184+
, test "aspect/tests: tests/aspect_test.ts uses Deno.test" $ do
185+
content <- readFileToString "tests/aspect_test.ts"
186+
assertTrue "Deno.test( present" (isInfixOf "Deno.test(" content)
187+
188+
, test "aspect/tests: tests/property_test.ts uses Deno.test" $ do
189+
content <- readFileToString "tests/property_test.ts"
190+
assertTrue "Deno.test( present" (isInfixOf "Deno.test(" content)
191+
192+
, test "aspect/tests: tests/smoke_test.ts uses Deno.test" $ do
193+
content <- readFileToString "tests/smoke_test.ts"
194+
assertTrue "Deno.test( present" (isInfixOf "Deno.test(" content)
195+
196+
, test "aspect/tests: tests/e2e_test.ts uses Deno.test" $ do
197+
content <- readFileToString "tests/e2e_test.ts"
198+
assertTrue "Deno.test( present" (isInfixOf "Deno.test(" content)
199+
]

0 commit comments

Comments
 (0)