test(jobs): 6 tests for recipe-level cert suppression + orphan gate

Six failing tests in test_recipe_cert_suppression.py covering the
full design surface:

  1. test_recipe_suppresses_thickness
  2. test_recipe_suppresses_nadcap_for_commodity_part
  3. test_recipe_cannot_add_certs_customer_didnt_want (suppress-only
     regression guard — recipe can never add types customer didn't ask for)
  4. test_part_override_coc_recipe_suppresses
  5. test_all_orphan_types_propagate (4-element output + bundling)
  6. test_orphan_cert_issue_blocks_without_attachment

These will all fail until T4 (resolver) and T5 (orphan-attach gate)
land. RED phase of TDD locked in via commit ordering.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-27 02:02:18 -04:00
parent a5063cc816
commit ae02164b78
2 changed files with 144 additions and 0 deletions

View File

@@ -8,3 +8,4 @@ from . import test_late_risk_ratio
from . import test_active_step_id
from . import test_autopause_cron
from . import test_post_shop_states
from . import test_recipe_cert_suppression

View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
"""Recipe-level cert suppression tests (spec 2026-05-27).
Verifies fp.job._resolve_required_cert_types respects the five
recipe-level requires_* Booleans (suppress-only precedence) and the
orphan-cert action_issue gate raises on missing attachment.
Spec: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Plan: docs/superpowers/plans/2026-05-27-recipe-cert-toggles-plan.md
"""
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
class TestRecipeCertSuppression(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env['res.partner'].create({
'name': 'AeroCustomer',
'is_company': True,
})
cls.product = cls.env['product.product'].create({'name': 'AeroPart'})
# Recipe = top-level fusion.plating.process.node with node_type='recipe'
cls.recipe = cls.env['fusion.plating.process.node'].create({
'name': 'TestRecipe',
'node_type': 'recipe',
})
_part_seq = 0
def _make_part(self, **kw):
# Bump per-test counter so multiple parts in the same test don't
# collide on the (partner_id, part_number) uniqueness constraint.
type(self)._part_seq += 1
vals = {
'name': 'PartA',
'part_number': 'PN-CERT-%03d' % self._part_seq,
'partner_id': self.partner.id,
'certificate_requirement': 'inherit',
}
vals.update(kw)
return self.env['fp.part.catalog'].create(vals)
def _make_job(self, **kw):
vals = {
'partner_id': self.partner.id,
'product_id': self.product.id,
'qty': 1.0,
'recipe_id': self.recipe.id,
}
vals.update(kw)
return self.env['fp.job'].create(vals)
# ---- Test 1: recipe suppresses thickness ----
def test_recipe_suppresses_thickness(self):
"""Customer wants thickness, recipe says no thickness -> only CoC."""
self.partner.x_fc_send_coc = True
self.partner.x_fc_send_thickness_report = True
self.recipe.requires_thickness_report = False
part = self._make_part()
job = self._make_job(part_catalog_id=part.id)
result = job._resolve_required_cert_types()
# CoC stays, thickness is suppressed.
self.assertEqual(result, {'coc'})
# ---- Test 2: recipe suppresses nadcap on commodity part ----
def test_recipe_suppresses_nadcap_for_commodity_part(self):
"""Customer wants nadcap, commodity recipe says no nadcap -> none."""
self.partner.x_fc_send_coc = True
self.partner.x_fc_send_nadcap_cert = True
self.recipe.requires_nadcap_cert = False
part = self._make_part()
job = self._make_job(part_catalog_id=part.id)
result = job._resolve_required_cert_types()
self.assertEqual(result, {'coc'})
self.assertNotIn('nadcap_cert', result)
# ---- Test 3: recipe cannot ADD what customer didn't want ----
def test_recipe_cannot_add_certs_customer_didnt_want(self):
"""Partner all OFF, recipe all True -> empty (suppress-only)."""
self.partner.x_fc_send_coc = False
self.partner.x_fc_send_thickness_report = False
self.partner.x_fc_send_nadcap_cert = False
self.partner.x_fc_send_mill_test = False
self.partner.x_fc_send_customer_specific = False
# All recipe requires_* default True; do not flip anything.
part = self._make_part()
job = self._make_job(part_catalog_id=part.id)
self.assertEqual(job._resolve_required_cert_types(), set())
# ---- Test 4: recipe can suppress part-level override ----
def test_part_override_coc_recipe_suppresses(self):
"""Part says coc, recipe says no coc -> empty (recipe wins)."""
self.recipe.requires_coc = False
part = self._make_part(certificate_requirement='coc')
job = self._make_job(part_catalog_id=part.id)
self.assertEqual(job._resolve_required_cert_types(), set())
# ---- Test 5: all 3 orphan types propagate when customer wants them ----
def test_all_orphan_types_propagate(self):
"""All 5 partner toggles ON, recipe default -> 4-element set
(thickness collapses into CoC via bundling rule)."""
self.partner.x_fc_send_coc = True
self.partner.x_fc_send_thickness_report = True
self.partner.x_fc_send_nadcap_cert = True
self.partner.x_fc_send_mill_test = True
self.partner.x_fc_send_customer_specific = True
part = self._make_part()
job = self._make_job(part_catalog_id=part.id)
result = job._resolve_required_cert_types()
# thickness merges into CoC PDF (bundling rule preserved)
self.assertEqual(
result,
{'coc', 'nadcap_cert', 'mill_test', 'customer_specific'},
)
self.assertNotIn('thickness_report', result)
# ---- Test 6: orphan cert blocks Issue without attachment ----
def test_orphan_cert_issue_blocks_without_attachment(self):
"""Spawn a Nadcap cert with no attachment -> action_issue raises."""
# Give the partner an email so the existing email-on-contact gate
# doesn't fire first; we want to verify the NEW gate explicitly.
self.partner.email = 'qa@aerocustomer.test'
cert = self.env['fp.certificate'].create({
'name': 'TEST-NADCAP-001',
'certificate_type': 'nadcap_cert',
'state': 'draft',
'partner_id': self.partner.id,
'contact_partner_id': self.partner.id,
'spec_reference': 'AMS 2404',
'process_description': 'TEST PROCESS',
'certified_by_id': self.env.user.id,
})
# Bypass the QM-only authority gate so the failure we see is the
# NEW gate, not the pre-existing authority check.
with self.assertRaisesRegex(UserError, 'no PDF attached'):
cert.with_context(
fp_skip_cert_authority_gate=True
).action_issue()