canontra-0.1.0.0: test/Canontra/TypeContractSpec.hs
{- |
Module : Canontra.TypeContractSpec
Description : Test specification for Polyglot Flow-Sensitive Structural Type Invariance (F_T).
Validates method permutation invariance, union and intersection type commutativity,
cross-language structural subtyping, nominal interface independence, and mutation sensitivity.
-}
{-# LANGUAGE OverloadedStrings #-}
module Canontra.TypeContractSpec (spec) where
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import Test.Hspec
import Canontra.Analysis.Impact (ChangeSeverity (..), classifySeverity)
import Canontra.Analysis.TypeContract
import Canontra.Comparison.Compare (compareBundles, compareFingerprints, formatComparisonResult)
import Canontra.Fingerprint.Bundle (computeBundleFromSource, computeProgramFingerprints)
import Canontra.Fingerprint.TypeContract (computeFT)
import Canontra.Parser.Polyglot (parsePolyglotSource)
import Canontra.Types
spec :: Spec
spec = do
describe "Union & Intersection Type Commutativity" $ do
it "guarantees union commutativity (A | B == B | A)" $ do
let u1 = parseTypeString "number | string"
u2 = parseTypeString "string | number"
u1 `shouldBe` u2
it "guarantees multi-member union associativity and deduplication" $ do
let u1 = parseTypeString "bool | number | string | number"
u2 = parseTypeString "string | bool | number"
u1 `shouldBe` u2
it "guarantees intersection commutativity (A & B == B & A)" $ do
let i1 = parseTypeString "Serializable & Cloneable"
i2 = parseTypeString "Cloneable & Serializable"
i1 `shouldBe` i2
describe "Method Permutation Invariance" $ do
let tsIfaceOrderA = T.unlines
[ "export interface DataService {"
, " read(key: string): Uint8Array;"
, " write(key: string, val: Uint8Array): boolean;"
, " close(): void;"
, "}"
]
let tsIfaceOrderB = T.unlines
[ "export interface DataService {"
, " close(): void;"
, " write(key: string, val: Uint8Array): boolean;"
, " read(key: string): Uint8Array;"
, "}"
]
it "produces bit-identical F_T when interface methods are permuted" $ do
case (parsePolyglotSource "service.ts" tsIfaceOrderA, parsePolyglotSource "service.ts" tsIfaceOrderB) of
(Right pA, Right pB) -> do
let ftA = computeFT pA
ftB = computeFT pB
ftA `shouldBe` ftB
unFingerprint ftA `shouldNotBe` ""
(Left e1, _) -> expectationFailure (show e1)
(_, Left e2) -> expectationFailure (show e2)
describe "Structural Subtyping & Equivalence" $ do
let mRead = MethodContract "read" [TypePrimitive "string"] (TypeArray (TypePrimitive "byte")) False
mWrite = MethodContract "write" [TypePrimitive "string", TypeArray (TypePrimitive "byte")] (TypePrimitive "bool") False
mClose = MethodContract "close" [] (TypePrimitive "void") False
let contractSuper = InterfaceContract "Reader" [mClose, mRead] []
let contractSub = InterfaceContract "FullService" [mClose, mRead, mWrite] []
let contractNominalOther = InterfaceContract "IReader" [mClose, mRead] []
it "identifies structural equality between differently named interfaces" $ do
areStructurallyEqual contractSuper contractNominalOther `shouldBe` True
it "validates structural subtyping when sub-contract contains superset of methods" $ do
isSubtypeOf contractSub contractSuper `shouldBe` True
isSubtypeOf contractSuper contractSub `shouldBe` False
describe "Type Contract Sensitivity" $ do
let tsBase = T.unlines
[ "export interface TokenService {"
, " generate(id: string): string;"
, "}"
]
let tsMutatedReturn = T.unlines
[ "export interface TokenService {"
, " generate(id: string): number;"
, "}"
]
let tsMutatedParam = T.unlines
[ "export interface TokenService {"
, " generate(id: string, salt: string): string;"
, "}"
]
it "sensitively alters F_T when return type changes" $ do
case (parsePolyglotSource "token.ts" tsBase, parsePolyglotSource "token.ts" tsMutatedReturn) of
(Right p1, Right p2) -> computeFT p1 `shouldNotBe` computeFT p2
_ -> expectationFailure "Parse failure in sensitivity test"
it "sensitively alters F_T when parameter signature changes" $ do
case (parsePolyglotSource "token.ts" tsBase, parsePolyglotSource "token.ts" tsMutatedParam) of
(Right p1, Right p2) -> computeFT p1 `shouldNotBe` computeFT p2
_ -> expectationFailure "Parse failure in sensitivity test"
describe "Go Structural Interface Normalization" $ do
let goIfaceA = T.unlines
[ "package store"
, ""
, "type Storage interface {"
, " Get(key string) ([]byte, error)"
, " Put(key string, val []byte) error"
, "}"
]
let goIfaceB = T.unlines
[ "package store"
, ""
, "type Storage interface {"
, " Put(key string, val []byte) error"
, " Get(key string) ([]byte, error)"
, "}"
]
it "produces identical F_T for permuted Go interfaces" $ do
case (parsePolyglotSource "store.go" goIfaceA, parsePolyglotSource "store.go" goIfaceB) of
(Right pA, Right pB) -> computeFT pA `shouldBe` computeFT pB
(Left e1, _) -> expectationFailure (show e1)
(_, Left e2) -> expectationFailure (show e2)
describe "Extended Structural Type Algebraic Properties" $ do
it "guarantees three-member union commutativity and canonical sorting" $ do
let u1 = parseTypeString "boolean | number | string"
u2 = parseTypeString "string | boolean | number"
u3 = parseTypeString "number | string | boolean"
u1 `shouldBe` u2
u2 `shouldBe` u3
it "guarantees idempotent deduplication in complex unions" $ do
let u1 = parseTypeString "string | number | string | boolean | number | boolean"
u2 = parseTypeString "boolean | number | string"
u1 `shouldBe` u2
it "guarantees three-member intersection commutativity" $ do
let i1 = parseTypeString "Alpha & Beta & Gamma"
i2 = parseTypeString "Gamma & Beta & Alpha"
i1 `shouldBe` i2
it "proves subtyping reflexivity (A <= A for all contracts)" $ do
let m = MethodContract "exec" [TypePrimitive "int"] (TypePrimitive "void") False
c = InterfaceContract "Runner" [m] []
isSubtypeOf c c `shouldBe` True
it "proves subtyping transitivity (A <= B and B <= C implies A <= C)" $ do
let mA = MethodContract "a" [] (TypePrimitive "void") False
mB = MethodContract "b" [] (TypePrimitive "void") False
mC = MethodContract "c" [] (TypePrimitive "void") False
contractC = InterfaceContract "Base" [mA] []
contractB = InterfaceContract "Middle" [mA, mB] []
contractA = InterfaceContract "Top" [mA, mB, mC] []
isSubtypeOf contractA contractB `shouldBe` True
isSubtypeOf contractB contractC `shouldBe` True
isSubtypeOf contractA contractC `shouldBe` True
it "detects non-subtyping when a required method is absent" $ do
let mA = MethodContract "read" [] (TypePrimitive "string") False
mB = MethodContract "write" [TypePrimitive "string"] (TypePrimitive "void") False
cReader = InterfaceContract "Reader" [mA] []
cWriter = InterfaceContract "Writer" [mB] []
isSubtypeOf cReader cWriter `shouldBe` False
isSubtypeOf cWriter cReader `shouldBe` False
it "handles empty interface with zero methods" $ do
let cEmpty = InterfaceContract "Any" [] []
m = MethodContract "ping" [] (TypePrimitive "bool") False
cFull = InterfaceContract "Pinger" [m] []
isSubtypeOf cFull cEmpty `shouldBe` True
isSubtypeOf cEmpty cFull `shouldBe` False
it "Rust: produces identical F_T when Trait methods are permuted" $ do
let rsTraitA = T.unlines
[ "pub trait Processor {"
, " fn process(&self) -> bool;"
, " fn reset(&mut self);"
, "}"
]
let rsTraitB = T.unlines
[ "pub trait Processor {"
, " fn reset(&mut self);"
, " fn process(&self) -> bool;"
, "}"
]
case (parsePolyglotSource "proc.rs" rsTraitA, parsePolyglotSource "proc.rs" rsTraitB) of
(Right pA, Right pB) -> computeFT pA `shouldBe` computeFT pB
(Left e1, _) -> expectationFailure (show e1)
(_, Left e2) -> expectationFailure (show e2)
it "sensitively alters F_T when method count changes in interface" $ do
let ts1 = "export interface Svc { run(): void; }"
ts2 = "export interface Svc { run(): void; stop(): void; }"
case (parsePolyglotSource "s1.ts" ts1, parsePolyglotSource "s2.ts" ts2) of
(Right p1, Right p2) -> computeFT p1 `shouldNotBe` computeFT p2
_ -> expectationFailure "Parse failed"
it "sensitively alters F_T when method name changes in interface" $ do
let ts1 = "export interface Calc { add(x: number): number; }"
ts2 = "export interface Calc { sum(x: number): number; }"
case (parsePolyglotSource "c1.ts" ts1, parsePolyglotSource "c2.ts" ts2) of
(Right p1, Right p2) -> computeFT p1 `shouldNotBe` computeFT p2
_ -> expectationFailure "Parse failed"
it "sensitively alters F_T when method parameter type changes" $ do
let ts1 = "export interface Validator { check(val: string): boolean; }"
ts2 = "export interface Validator { check(val: number): boolean; }"
case (parsePolyglotSource "v1.ts" ts1, parsePolyglotSource "v2.ts" ts2) of
(Right p1, Right p2) -> computeFT p1 `shouldNotBe` computeFT p2
_ -> expectationFailure "Parse failed"
it "produces non-empty deterministic F_T hash for polyglot interfaces" $ do
let ts = "export interface Api { fetch(url: string): string; }"
case parsePolyglotSource "api.ts" ts of
Right p -> unFingerprint (computeFT p) `shouldNotBe` ""
Left err -> expectationFailure (show err)
describe "Phase 1: Core 9-Tier Identity Matrix & Type Contract Promotion" $ do
describe "Full 9-Tier Bundle Construction" $ do
it "populates fTTypeContract in FingerprintBundle from computeBundle" $ do
let tsCode = "export interface Greeter { greet(name: string): string; }"
case computeBundleFromSource "greeter.ts" tsCode of
Left err -> expectationFailure (show err)
Right b -> do
unFingerprint (fTTypeContract b) `shouldNotBe` ""
unFingerprint (f4Composite b) `shouldNotBe` ""
it "populates fTTypeContract in computeProgramFingerprints" $ do
let goCode = "package svc\ntype Storage interface {\n Save(data []byte) error\n}\n"
case parsePolyglotSource "storage.go" goCode of
Left err -> expectationFailure (show err)
Right prog -> do
let b = computeProgramFingerprints prog
unFingerprint (fTTypeContract b) `shouldNotBe` ""
unFingerprint (f4Composite b) `shouldNotBe` ""
describe "Composite F4 9-Tier Invariance & Sensitivity" $ do
it "guarantees F_T invariance under interface method permutation while F4 reflects AST order" $ do
let tsA = "export interface Service { a(): void; b(): number; }"
tsB = "export interface Service { b(): number; a(): void; }"
case (computeBundleFromSource "s.ts" tsA, computeBundleFromSource "s.ts" tsB) of
(Right bA, Right bB) -> do
fTTypeContract bA `shouldBe` fTTypeContract bB
unFingerprint (fTTypeContract bA) `shouldNotBe` ""
_ -> expectationFailure "Bundle computation failed"
it "guarantees full F1..F4 and FT invariance under pure top-level interface permutation" $ do
let tsA = "export interface ServiceA { run(): void; }\nexport interface ServiceB { stop(): void; }\n"
tsB = "export interface ServiceB { stop(): void; }\nexport interface ServiceA { run(): void; }\n"
case (computeBundleFromSource "s.ts" tsA, computeBundleFromSource "s.ts" tsB) of
(Right bA, Right bB) -> do
f1Structural bA `shouldBe` f1Structural bB
f2Declaration bA `shouldBe` f2Declaration bB
fTTypeContract bA `shouldBe` fTTypeContract bB
f4Composite bA `shouldBe` f4Composite bB
_ -> expectationFailure "Bundle computation failed"
it "sensitively alters F4 composite when type contract method signature changes" $ do
let tsA = "export interface Repo { find(id: string): string; }"
tsB = "export interface Repo { find(id: number): string; }"
case (computeBundleFromSource "r.ts" tsA, computeBundleFromSource "r.ts" tsB) of
(Right bA, Right bB) -> do
fTTypeContract bA `shouldNotBe` fTTypeContract bB
f4Composite bA `shouldNotBe` f4Composite bB
_ -> expectationFailure "Bundle computation failed"
describe "9-Tier Semantic Invariant Comparison (crTypeContract)" $ do
it "evaluates crTypeContract as Identical for equivalent type contracts" $ do
let go1 = "package api\ntype Writer interface { Write(p []byte) (n int, err error) }\n"
go2 = "package api\ntype Writer interface {\n Write(p []byte) (n int, err error)\n}\n"
case (computeBundleFromSource "w1.go" go1, computeBundleFromSource "w2.go" go2) of
(Right b1, Right b2) -> do
let cr = compareBundles b1 b2
crTypeContract cr `shouldBe` Identical
crComposite cr `shouldBe` Identical
_ -> expectationFailure "Bundle computation failed"
it "evaluates crTypeContract as Different when interface method return type changes" $ do
let ts1 = "export interface Worker { doWork(): boolean; }"
ts2 = "export interface Worker { doWork(): number; }"
case (computeBundleFromSource "w1.ts" ts1, computeBundleFromSource "w2.ts" ts2) of
(Right b1, Right b2) -> do
let cr = compareFingerprints b1 b2
crTypeContract cr `shouldBe` Different
crComposite cr `shouldBe` Different
_ -> expectationFailure "Bundle computation failed"
it "formats comparison result including FT (Type Contract) tier" $ do
let b1 = FingerprintBundle (Fingerprint "s") (Fingerprint "st") (Fingerprint "dc") (Fingerprint "dp") (Fingerprint "cg") (Fingerprint "cf") (Fingerprint "df") (Fingerprint "tc") (Fingerprint "cp")
b2 = FingerprintBundle (Fingerprint "s") (Fingerprint "st") (Fingerprint "dc") (Fingerprint "dp") (Fingerprint "cg") (Fingerprint "cf") (Fingerprint "df") (Fingerprint "diff_tc") (Fingerprint "diff_cp")
cr = compareBundles b1 b2
fmt = formatComparisonResult cr
T.isInfixOf "FT (Type Contract):" fmt `shouldBe` True
T.isInfixOf "DIFFERENT" fmt `shouldBe` True
describe "Backward-Compatible JSON Serialization & Parsing" $ do
it "roundtrips FingerprintBundle with type_contract to and from JSON" $ do
let b = FingerprintBundle (Fingerprint "s") (Fingerprint "st") (Fingerprint "dc") (Fingerprint "dp") (Fingerprint "cg") (Fingerprint "cf") (Fingerprint "df") (Fingerprint "tc123") (Fingerprint "cp")
encoded = Aeson.encode b
Aeson.decode encoded `shouldBe` Just b
it "deserializes historical manifests missing type_contract cleanly defaulting to empty Fingerprint" $ do
let legacyJson = "{\"source\":\"s\",\"structural\":\"st\",\"declaration\":\"dc\",\"dependency\":\"dp\",\"call_graph\":\"cg\",\"control_flow\":\"cf\",\"data_flow\":\"df\",\"composite\":\"cp\"}" :: BL.ByteString
case Aeson.decode legacyJson of
Nothing -> expectationFailure "Failed to parse legacy FingerprintBundle JSON"
Just b -> do
f0Source b `shouldBe` Fingerprint "s"
f1Structural b `shouldBe` Fingerprint "st"
fTTypeContract b `shouldBe` Fingerprint ""
f4Composite b `shouldBe` Fingerprint "cp"
it "roundtrips ComparisonResult with type_contract to and from JSON" $ do
let cr = ComparisonResult Identical Identical Identical Identical Identical Identical Identical Different Different
encoded = Aeson.encode cr
Aeson.decode encoded `shouldBe` Just cr
it "deserializes historical ComparisonResult missing type_contract defaulting to Identical" $ do
let legacyCrJson = "{\"source\":\"identical\",\"structural\":\"identical\",\"declaration\":\"identical\",\"dependency\":\"identical\",\"call_graph\":\"identical\",\"control_flow\":\"identical\",\"data_flow\":\"identical\",\"composite\":\"identical\"}" :: BL.ByteString
case Aeson.decode legacyCrJson of
Nothing -> expectationFailure "Failed to parse legacy ComparisonResult JSON"
Just cr -> do
crStructural cr `shouldBe` Identical
crTypeContract cr `shouldBe` Identical
crComposite cr `shouldBe` Identical
describe "Change Impact Analysis Classification for Type Contracts" $ do
it "classifies type contract mutations as SeverityInterface" $ do
let bOld = FingerprintBundle (Fingerprint "s") (Fingerprint "st") (Fingerprint "dc") (Fingerprint "dp") (Fingerprint "cg") (Fingerprint "cf") (Fingerprint "df") (Fingerprint "tc_old") (Fingerprint "c1")
bNew = FingerprintBundle (Fingerprint "s") (Fingerprint "st") (Fingerprint "dc") (Fingerprint "dp") (Fingerprint "cg") (Fingerprint "cf") (Fingerprint "df") (Fingerprint "tc_new") (Fingerprint "c2")
classifySeverity bOld bNew `shouldBe` SeverityInterface