Peppol sending has been live since #1780 behind a per-company access grant, but the MCP skills, the swedish-invoice-compliance atom, docs/PEPPOL_FOUNDATION.md and the v1 :send / :mark-sent descriptions still told agents it did not exist. Every text now says gated per company (requested under Installningar > Fakturering) and keeps the restrictions explicit: aktiebolag senders, standard invoices only, Swedish org-number buyers, no MCP or v1 Peppol send verb yet, :mark-sent as the recovery step when a network-accepted send fails issuance. The skills guard test pins the truthful claim across all surfaces. Includes the regenerated agent_atom_registry seeds and skills/accounted-api references. Refs #546
This commit is contained in:
@@ -140,23 +140,112 @@ describe('Skills registry', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not advertise unsupported built-in e-invoice delivery', () => {
|
||||
it('describes Peppol sending truthfully: gated per company, never absent (#546)', () => {
|
||||
const invoiceComplianceAtom = readFileSync(
|
||||
join(process.cwd(), '.claude/skills/swedish-invoice-compliance/SKILL.md'),
|
||||
'utf8',
|
||||
)
|
||||
const allBodies = [...skills.map((skill) => skill.body), invoiceComplianceAtom].join('\n')
|
||||
|
||||
expect(allBodies).not.toMatch(/Accounted\s+(?:renders|generates|produces)[^.\n]*EN\s*16931/i)
|
||||
expect(allBodies).not.toMatch(/Accounted\s+(?:handles|sends|delivers)[^.\n]*Peppol/i)
|
||||
// The release-pinned EN 16931 validation stack is still open (docs/PEPPOL_FOUNDATION.md),
|
||||
// so no text may claim Accounted validates against it.
|
||||
expect(allBodies).not.toMatch(/Accounted\s+(?:renders|generates|produces|validates)[^.\n]*EN\s*16931/i)
|
||||
// Peppol send is live behind a per-company access grant (app/api/invoices/[id]/peppol/send).
|
||||
// A text that claims the capability is absent sends users to a competitor; say gated instead.
|
||||
// The patterns take the capability as subject (active, passive and adjective forms) so that
|
||||
// the true statement "a v1 or MCP Peppol send action is not yet available" stays legal.
|
||||
const capabilityAbsentPatterns = [
|
||||
/(?:does not|doesn't|cannot|can't)[^.\n]*(?:send|deliver|generate)[^.\n]*Peppol/i,
|
||||
/Peppol\s+(?:invoices?|e-invoices?|documents?|send(?:ing)?|delivery)\s+(?:is|are|has|have)\s+(?:not|never)\b[^.\n]*\b(?:sent|delivered|generated|built|implemented|available|supported|possible)\b/i,
|
||||
]
|
||||
for (const pattern of capabilityAbsentPatterns) {
|
||||
expect(allBodies).not.toMatch(pattern)
|
||||
// Forbidden: the pre-#546 wording in its active, passive and adjective forms.
|
||||
expect(
|
||||
[
|
||||
'It does not generate e-invoice XML or deliver invoices through Peppol.',
|
||||
'Peppol invoices are not generated or sent by Accounted.',
|
||||
'Peppol sending has not been built.',
|
||||
'Peppol sending is not yet available.',
|
||||
].some((claim) => pattern.test(claim)),
|
||||
).toBe(true)
|
||||
// Legal: the v1 :send/:mark-sent descriptions say the agent verb is missing, not the capability.
|
||||
expect('a v1 or MCP Peppol send action is not yet available').not.toMatch(pattern)
|
||||
}
|
||||
// No MCP tool or v1 action sends via Peppol yet: no text may hand an agent a Peppol send verb.
|
||||
expect(allBodies).not.toMatch(/gnubok_send_invoice[^.\n]*Peppol/i)
|
||||
expect(allBodies).not.toMatch(/gnubok_send_peppol|gnubok_peppol_send/i)
|
||||
|
||||
for (const slug of ['invoicing-rules', 'customer-onboarding']) {
|
||||
const truthfulSkills = ['invoicing-rules', 'customer-onboarding'].map((slug) => {
|
||||
const skill = skills.find((candidate) => candidate.slug === slug)
|
||||
expect(skill?.body).toMatch(/external e-invoice provider/i)
|
||||
expect(skill?.body).toContain('gnubok_mark_invoice_as_sent')
|
||||
expect(skill, `skill ${slug}`).toBeTruthy()
|
||||
return skill!
|
||||
})
|
||||
// The discovery surface (gnubok_list_skills) must not frame e-invoicing as external either.
|
||||
for (const skill of truthfulSkills) {
|
||||
expect(skill.summary).not.toMatch(/external e-invoic/i)
|
||||
}
|
||||
expect(truthfulSkills[0].summary).toMatch(/Peppol/)
|
||||
|
||||
const truthfulTexts = [...truthfulSkills.map((skill) => skill.body), invoiceComplianceAtom]
|
||||
for (const text of truthfulTexts) {
|
||||
// Where it lives, and that it is gated per company (with the English label for en-locale users).
|
||||
expect(text).toMatch(/invoice page in the dashboard/i)
|
||||
expect(text).toMatch(/(?:gated|access)[^.\n]*per company|per[- ]company[^.\n]*(?:access|gated)/i)
|
||||
expect(text).toContain('Inställningar > Fakturering (Settings > Invoicing)')
|
||||
expect(text).toMatch(/send cap/i)
|
||||
// The restrictions agents must not over-promise past (lib/invoices/peppol-bis-billing.ts).
|
||||
expect(text).toMatch(/aktiebolag/i)
|
||||
expect(text).toMatch(/enskild firma/i)
|
||||
expect(text).toMatch(/standard invoices only|no credit notes/i)
|
||||
expect(text).toMatch(/SEK/)
|
||||
expect(text).toMatch(/6, 12 or 25 %/)
|
||||
expect(text).toMatch(/no reverse charge/i)
|
||||
expect(text).toMatch(/no ROT\/RUT deductions/i)
|
||||
expect(text).toMatch(/Er referens/)
|
||||
// No agent-callable send verb yet.
|
||||
expect(text).toMatch(/no MCP tool[^.\n]*Peppol|MCP tool[^.\n]*not (?:yet )?available/i)
|
||||
// A successful dashboard send issues the invoice; mark-sent is only the issuance-failure recovery.
|
||||
expect(text).toMatch(/successful dashboard Peppol send issues the invoice itself/i)
|
||||
expect(text).toMatch(/could not be marked as sent/i)
|
||||
// The fallback path stays documented for companies without access.
|
||||
expect(text).toMatch(/external e-invoice provider/i)
|
||||
expect(text).toContain('gnubok_mark_invoice_as_sent')
|
||||
// The exporter refuses personnummer-based BUYER identifiers too (prepareParty('buyer') in
|
||||
// peppol-bis-billing.ts), so an enskild firma customer must not be promised a send.
|
||||
expect(text).toMatch(/(?:buyer|customer)[^.\n]*personnummer/i)
|
||||
// The mark-sent recovery applies to the still-draft invoice only: INVOICE_MARK_SENT_REPAIR_REQUIRED
|
||||
// leaves the invoice sent with the verifikat posted, and a second mark-sent returns 409.
|
||||
expect(text).toMatch(/still-draft invoice/i)
|
||||
}
|
||||
|
||||
// The numbered workflow must route an e-invoice customer to the Peppol section from Step 4
|
||||
// itself, so an agent reading top-down never reaches the external-provider fallback first.
|
||||
const invoicingRules = truthfulSkills[0].body
|
||||
expect(invoicingRules).toMatch(/### Step 4: Send[\s\S]*?Peppol[\s\S]*?### Step 5/)
|
||||
// Kontantmetod and defer_invoice_booking companies get no verifikat at issue (Step 3 says the same).
|
||||
expect(invoicingRules).toMatch(/issues the invoice itself \(number, status, and the verifikat under faktureringsmetoden\)/)
|
||||
|
||||
// The v1 :send / :mark-sent descriptions (source of skills/accounted-api/references/invoices.md)
|
||||
// are the fourth and fifth corrected surfaces; apiskill:check only detects generated-vs-source
|
||||
// drift, so the truthful claim is pinned here on the route source itself.
|
||||
const v1RouteTexts = [
|
||||
'app/api/v1/companies/[companyId]/invoices/[id]/send/route.ts',
|
||||
'app/api/v1/companies/[companyId]/invoices/[id]/mark-sent/route.ts',
|
||||
].map((relativePath) => readFileSync(join(process.cwd(), relativePath), 'utf8'))
|
||||
for (const text of v1RouteTexts) {
|
||||
for (const pattern of capabilityAbsentPatterns) {
|
||||
expect(text).not.toMatch(pattern)
|
||||
}
|
||||
// The pre-#546 framing listed Peppol as an external channel next to postal mail.
|
||||
expect(text).not.toMatch(/\(Peppol, postal/)
|
||||
expect(text).toMatch(/a v1 or MCP Peppol send action is not yet available/)
|
||||
expect(text).toMatch(/per-company access grant/)
|
||||
expect(text).toContain('Inställningar > Fakturering (Settings > Invoicing)')
|
||||
expect(text).toMatch(/aktiebolag senders, standard invoices only/)
|
||||
expect(text).toMatch(/buyers whose org number is not a personnummer/)
|
||||
expect(text).toMatch(/could not be marked as sent/)
|
||||
}
|
||||
expect(invoiceComplianceAtom).toMatch(/external e-invoice provider/i)
|
||||
expect(invoiceComplianceAtom).toContain('gnubok_mark_invoice_as_sent')
|
||||
})
|
||||
|
||||
it('findSkill resolves the workflow skill or null (sync workflow lookup)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user