feat: Template-Spec v1 Phase C — IF-Renderer + HOSTING/FEATURES + 4 neue DE-Templates
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
- contextBridge.ts: HostingCtx + FeaturesCtx (35 Felder), ~50 neue Platzhalter-Aliases - ruleEngine.ts: buildBoolContext() + applyConditionalBlocks() (IF/IF_NOT/IF_ANY) - ruleEngine.test.ts: 67 Tests (+18 für Phase C), alle grün - page.tsx: IF-Renderer in Pipeline, HOSTING+FEATURES Formular-Sections, erweiterter SDK-Prefill - scripts/apply_templates_023.py: 4 neue DE-Templates (Cookie v2, DSE, AGB, Impressum) - migrations/023_new_templates_de.sql: Dokumentation + Verifikations-Query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ import {
|
||||
runRuleset,
|
||||
getDocType,
|
||||
applyBlockRemoval,
|
||||
buildBoolContext,
|
||||
applyConditionalBlocks,
|
||||
type RuleInput,
|
||||
} from './ruleEngine'
|
||||
import { EMPTY_CONTEXT } from './contextBridge'
|
||||
@@ -497,3 +499,166 @@ describe('integration', () => {
|
||||
expect(r.computedFlags.HAS_MARKETING).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// buildBoolContext
|
||||
// =============================================================================
|
||||
|
||||
describe('buildBoolContext', () => {
|
||||
it('maps computed flags directly', () => {
|
||||
const ctx = withCtx({
|
||||
CUSTOMER: { ...EMPTY_CONTEXT.CUSTOMER, IS_CONSUMER: true, IS_BUSINESS: false },
|
||||
SERVICE: { ...EMPTY_CONTEXT.SERVICE, MODEL: 'SaaS' },
|
||||
})
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.IS_B2C).toBe(true)
|
||||
expect(boolCtx.IS_B2B).toBe(false)
|
||||
expect(boolCtx.SERVICE_IS_SAAS).toBe(true)
|
||||
expect(boolCtx.ANALYTICS_ENABLED).toBe(false)
|
||||
})
|
||||
|
||||
it('HAS_REGISTER derived from PROVIDER.REGISTER_COURT', () => {
|
||||
const ctx = withCtx({ PROVIDER: { ...EMPTY_CONTEXT.PROVIDER, REGISTER_COURT: 'AG München' } })
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.HAS_REGISTER).toBe(true)
|
||||
})
|
||||
|
||||
it('HAS_REGISTER=false when REGISTER_COURT is empty', () => {
|
||||
const ctx = withCtx({ PROVIDER: { ...EMPTY_CONTEXT.PROVIDER, REGISTER_COURT: '' } })
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.HAS_REGISTER).toBe(false)
|
||||
})
|
||||
|
||||
it('HAS_VAT_ID derived from PROVIDER.VAT_ID', () => {
|
||||
const ctx = withCtx({ PROVIDER: { ...EMPTY_CONTEXT.PROVIDER, VAT_ID: 'DE123456789' } })
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.HAS_VAT_ID).toBe(true)
|
||||
})
|
||||
|
||||
it('HAS_DPO derived from PRIVACY.DPO_NAME', () => {
|
||||
const ctx = withCtx({ PRIVACY: { ...EMPTY_CONTEXT.PRIVACY, DPO_NAME: 'Max Mustermann' } })
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.HAS_DPO).toBe(true)
|
||||
})
|
||||
|
||||
it('FEATURES booleans are passed through', () => {
|
||||
const ctx = withCtx({
|
||||
FEATURES: {
|
||||
...EMPTY_CONTEXT.FEATURES,
|
||||
HAS_NEWSLETTER: true,
|
||||
HAS_ACCOUNT: true,
|
||||
HAS_PAID_PLANS: false,
|
||||
HAS_THIRD_COUNTRY: true,
|
||||
},
|
||||
})
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.HAS_NEWSLETTER).toBe(true)
|
||||
expect(boolCtx.HAS_ACCOUNT).toBe(true)
|
||||
expect(boolCtx.HAS_PAID_PLANS).toBe(false)
|
||||
expect(boolCtx.THIRD_COUNTRY_POSSIBLE).toBe(true)
|
||||
})
|
||||
|
||||
it('MARKETING_ENABLED is alias for HAS_MARKETING', () => {
|
||||
const ctx = withCtx({ CONSENT: { ...EMPTY_CONTEXT.CONSENT, MARKETING_PARTNERS: 'Meta Pixel' } })
|
||||
const r = runRuleset(makeInput({ context: ctx }))
|
||||
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
|
||||
expect(boolCtx.HAS_MARKETING).toBe(true)
|
||||
expect(boolCtx.MARKETING_ENABLED).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// applyConditionalBlocks
|
||||
// =============================================================================
|
||||
|
||||
describe('applyConditionalBlocks', () => {
|
||||
it('{{#IF}} shows block when condition is true', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF HAS_ANALYTICS}}\nAnalytics section\n{{/IF}}',
|
||||
{ HAS_ANALYTICS: true }
|
||||
)
|
||||
expect(result).toContain('Analytics section')
|
||||
expect(result).not.toContain('{{#IF')
|
||||
})
|
||||
|
||||
it('{{#IF}} hides block when condition is false', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF HAS_ANALYTICS}}\nAnalytics section\n{{/IF}}',
|
||||
{ HAS_ANALYTICS: false }
|
||||
)
|
||||
expect(result).toBe('')
|
||||
expect(result).not.toContain('Analytics section')
|
||||
})
|
||||
|
||||
it('{{#IF_NOT}} shows block when condition is false', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF_NOT HAS_DPO}}\nKein DSB bestellt.\n{{/IF_NOT}}',
|
||||
{ HAS_DPO: false }
|
||||
)
|
||||
expect(result).toContain('Kein DSB bestellt.')
|
||||
})
|
||||
|
||||
it('{{#IF_NOT}} hides block when condition is true', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF_NOT HAS_DPO}}\nKein DSB bestellt.\n{{/IF_NOT}}',
|
||||
{ HAS_DPO: true }
|
||||
)
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('{{#IF_ANY}} shows block when any condition is true', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF_ANY HAS_ANALYTICS HAS_MARKETING}}\nTracking aktiv.\n{{/IF_ANY}}',
|
||||
{ HAS_ANALYTICS: false, HAS_MARKETING: true }
|
||||
)
|
||||
expect(result).toContain('Tracking aktiv.')
|
||||
})
|
||||
|
||||
it('{{#IF_ANY}} hides block when all conditions are false', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF_ANY HAS_ANALYTICS HAS_MARKETING}}\nTracking aktiv.\n{{/IF_ANY}}',
|
||||
{ HAS_ANALYTICS: false, HAS_MARKETING: false }
|
||||
)
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('processes multiple blocks independently', () => {
|
||||
const content = '{{#IF A}}\nBlock A\n{{/IF}}\n{{#IF_NOT B}}\nBlock B\n{{/IF_NOT}}'
|
||||
const result = applyConditionalBlocks(content, { A: true, B: false })
|
||||
expect(result).toContain('Block A')
|
||||
expect(result).toContain('Block B')
|
||||
})
|
||||
|
||||
it('handles multiline block content', () => {
|
||||
const content = '{{#IF HAS_DPO}}\nName: {{DPO_NAME}}\nEmail: {{DPO_EMAIL}}\n{{/IF}}'
|
||||
const result = applyConditionalBlocks(content, { HAS_DPO: true })
|
||||
expect(result).toContain('Name: {{DPO_NAME}}')
|
||||
expect(result).toContain('Email: {{DPO_EMAIL}}')
|
||||
})
|
||||
|
||||
it('preserves placeholders inside kept blocks for later substitution', () => {
|
||||
const content = '{{#IF HAS_ANALYTICS}}\nTool: {{ANALYTICS_TOOLS_LIST}}\n{{/IF}}'
|
||||
const result = applyConditionalBlocks(content, { HAS_ANALYTICS: true })
|
||||
expect(result).toContain('{{ANALYTICS_TOOLS_LIST}}')
|
||||
})
|
||||
|
||||
it('is a no-op when content has no conditional directives', () => {
|
||||
const plain = '# Impressum\n\n{{COMPANY_LEGAL_NAME}}'
|
||||
expect(applyConditionalBlocks(plain, {})).toBe(plain)
|
||||
})
|
||||
|
||||
it('handles unknown condition as false (not in boolCtx)', () => {
|
||||
const result = applyConditionalBlocks(
|
||||
'{{#IF UNKNOWN_COND}}\nHidden\n{{/IF}}',
|
||||
{}
|
||||
)
|
||||
expect(result).toBe('')
|
||||
expect(result).not.toContain('Hidden')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user