From 64d937f0dc5dc301ca6219884b8522d24c9659d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 20:08:11 +0000 Subject: [PATCH] test(dogfood): pin what the reads serve for a dependent's email template locale the base does not ship A shape pin, measured on the showcase: a dependent package publishing the base template's name at a locale the base does not ship is served by the read naming the base and by the package-less read (stamped with the dependent's package id), and left out of the env-wide, base-scoped and executing lists, so a send in that locale renders the base's en-US. The cases marked SHAPE record today's answer pending the seat's decision. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FEkQ5pFHjQjFRsVAurERSJ --- ...dent-unshipped-locale-read.dogfood.test.ts | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts diff --git a/packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts b/packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts new file mode 100644 index 00000000000..c18399a41ea --- /dev/null +++ b/packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts @@ -0,0 +1,190 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [#28779, ADR-0146 D2 / D8, #28390] SHAPE PIN — documents what the reads +// serve TODAY when a dependent package (not the base's `.custom` pair) ships +// an email template under a base template's name at a locale the base does +// not ship. Over the real showcase composition and its HTTP doors. +// +// ⛔ This file records a measured shape; it does not assert the shape is +// right. Whether a dependent's member may be served on the base's address is +// open (#28779 hand-back, evidence on #28390). When that is decided, the +// cases marked SHAPE below move with the ruling — that is their purpose. +// +// ## The rig +// +// The showcase ships `showcase_task_done_email` in `en-US` only. A Studio-made +// package `com.example.showcaseaddon` declares the showcase as a dependency +// and publishes the same name at `fr-FR`. Since PR #28777 the write gate +// judges an email template by `(name, locale)`, so this is admitted as an +// addition (the dependent's `en-US` at the same name is still refused). +// +// ## What reads today (measured on main 8f294e04) +// +// - the read naming the dependent serves its `fr-FR`, stamped with its own +// package id — as expected; +// - SHAPE: the read naming the BASE (`?package=&member=fr-FR`) and the +// package-less read (`?member=fr-FR`) also serve the dependent's body, +// stamped `_packageId: `, `_provenance: 'org'`. The address +// finds no row of the base, its pair or no package at `fr-FR`, and the +// registry's by-name read falls to its cross-package tier +// (`SchemaRegistry.getItem`, ADR-0048 §3.3 first composite match); +// - SHAPE: the env-wide list, the list scoped to the base and the executing +// list (`getMetaItemsForExecution`) leave the `fr-FR` out — the env-wide +// narrowing keeps a name one code package ships to that package's items, +// at NAME grain — so a send in `fr-FR` renders the base's `en-US`. +// +// So the by-name reads and the lists disagree about the base's `fr-FR`, and +// the body no read stamps as the base's is still answered for the base's +// address. Controls: the base's own `en-US` reads as the base's on every +// read, and the pair's declared override of `en-US` reads as the override. + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { type VerifyStack } from '@objectstack/verify'; +import { bootShowcase } from './showcase-boot.js'; + +const BASE = 'com.example.showcase'; +const PAIR = 'com.example.showcase.custom'; +const DEPENDENT = 'com.example.showcaseaddon'; +/** The template the showcase ships, in `en-US` only. */ +const NAME = 'showcase_task_done_email'; +const SHIPPED_SUBJECT = '✅ Task done: {{title}}'; +const DEPENDENT_SUBJECT = 'Add-on FR: {{title}}'; +const OVERRIDE_SUBJECT = 'Override: {{title}}'; +/** The refusal's code — the save door answers it beside a string `error`, an enveloped door inside `error`. */ +const codeOf = (json: any): unknown => json?.code ?? json?.error?.code; + +interface Served { + status: number; + subject?: unknown; + locale?: unknown; + _packageId?: unknown; +} + +describe('[#28779] SHAPE: what the reads serve for a dependent\'s email template locale the base does not ship (showcase)', () => { + let stack: VerifyStack | undefined; + let token: string; + let shipped: Record; + + const call = async (method: string, path: string, body?: unknown) => { + const res = await stack!.apiAs(token, method, path, body); + return { status: res.status, json: (await res.json().catch(() => ({}))) as any }; + }; + /** `GET /meta/email_template/:name` at one member, with or without `?package=` — the served document, trimmed. */ + const byName = async (member: string, pkg?: string): Promise => { + const read = await call('GET', `/meta/email_template/${NAME}?member=${member}${pkg ? `&package=${pkg}` : ''}`); + if (read.status !== 200) return { status: read.status }; + const doc = read.json?.data ?? read.json?.item ?? read.json; + const item = (doc?.item ?? doc) as Record; + return { status: read.status, subject: item.subject, locale: item.locale, _packageId: item._packageId }; + }; + /** The template's rows on `GET /meta/email_template[?package=]`, as `[locale, _packageId, subject]`. */ + const listed = async (pkg?: string) => { + const list = await call('GET', `/meta/email_template${pkg ? `?package=${pkg}` : ''}`); + expect(list.status).toBe(200); + const items: any[] = Array.isArray(list.json) ? list.json : (list.json?.items ?? list.json?.data?.items ?? list.json?.data ?? []); + return items.filter((i) => i?.name === NAME).map((i) => [i.locale, i._packageId, i.subject]); + }; + /** The executing list the email template loader reads (`protocol.getMetaItemsForExecution`). */ + const executing = async () => { + const protocol: any = await stack!.kernel.getServiceAsync('protocol'); + const out = await protocol.getMetaItemsForExecution({ type: 'email_template' }); + return ((out?.items ?? []) as any[]).filter((i) => i?.name === NAME).map((i) => [i.locale, i._packageId, i.subject]); + }; + /** What a send in `locale` renders: `IEmailService.renderTemplate`, the resolver `sendTemplate` uses. */ + const rendered = async (locale: string) => { + const email: any = await stack!.kernel.getServiceAsync('email'); + return (await email.renderTemplate({ template: NAME, locale, data: { title: 'T' } }))?.subject; + }; + const body = (locale: string, subject: string, extra: Record = {}) => { + const out: Record = {}; + for (const [k, v] of Object.entries(shipped)) if (!k.startsWith('_')) out[k] = v; + return { ...out, locale, subject, ...extra }; + }; + const saveInto = (pkg: string, b: Record) => + call('PUT', `/meta/email_template/${NAME}?mode=draft&package=${pkg}`, b); + const publishFrom = (pkg: string, member: string) => + call('POST', `/meta/email_template/${NAME}/publish?package=${pkg}&member=${member}`, {}); + + const BASE_EN = { status: 200, subject: SHIPPED_SUBJECT, locale: 'en-US', _packageId: BASE }; + const DEPENDENT_FR = { status: 200, subject: DEPENDENT_SUBJECT, locale: 'fr-FR', _packageId: DEPENDENT }; + + beforeAll(async () => { + stack = await bootShowcase(); + token = await stack.signIn(); + const read = await call('GET', `/meta/email_template/${NAME}?member=en-US`); + expect(read.status).toBe(200); + const doc = read.json?.data ?? read.json?.item ?? read.json; + shipped = doc?.item ?? doc; + }, 180_000); + + afterAll(async () => { + await stack?.stop(); + }); + + it('baseline: nothing answers `fr-FR`, and the base\'s `en-US` reads as the base\'s', async () => { + expect(await byName('fr-FR', BASE)).toEqual({ status: 404 }); + expect(await byName('fr-FR')).toEqual({ status: 404 }); + expect(await byName('en-US', BASE)).toEqual(BASE_EN); + expect(await byName('en-US')).toEqual(BASE_EN); + expect(await rendered('fr-FR')).toBe('✅ Task done: T'); + }); + + it('the dependent publishes `fr-FR` under the base\'s name, admitted as an addition; its `en-US` is refused', async () => { + const created = await call('POST', '/packages', { + manifest: { + id: DEPENDENT, name: 'Showcase add-on', version: '0.1.0', type: 'app', namespace: 'scaddon', + dependencies: { [BASE]: '*' }, + }, + }); + expect(created.status, JSON.stringify(created.json)).toBe(201); + + const saved = await saveInto(DEPENDENT, body('fr-FR', DEPENDENT_SUBJECT)); + expect(saved.status, JSON.stringify(saved.json)).toBe(200); + const published = await publishFrom(DEPENDENT, 'fr-FR'); + expect(published.status, JSON.stringify(published.json)).toBe(200); + + // Control: the member the base ships is still a same-name collision. + const refused = await saveInto(DEPENDENT, body('en-US', 'Add-on EN: {{title}}')); + expect(refused.status, JSON.stringify(refused.json)).toBe(403); + expect(codeOf(refused.json)).toBe('NOT_OVERRIDABLE'); + }); + + it('the read naming the dependent serves its `fr-FR` under its own package id', async () => { + expect(await byName('fr-FR', DEPENDENT)).toEqual(DEPENDENT_FR); + expect(await listed(DEPENDENT)).toEqual([['fr-FR', DEPENDENT, DEPENDENT_SUBJECT]]); + }); + + it('SHAPE: the read naming the base and the package-less read serve the dependent\'s `fr-FR` body (stamped with the dependent)', async () => { + expect(await byName('fr-FR', BASE)).toEqual(DEPENDENT_FR); + expect(await byName('fr-FR')).toEqual(DEPENDENT_FR); + // Control: the base's own member is untouched on every read. + expect(await byName('en-US', BASE)).toEqual(BASE_EN); + expect(await byName('en-US')).toEqual(BASE_EN); + }); + + it('SHAPE: the env-wide, base-scoped and executing lists leave `fr-FR` out, so a `fr-FR` send renders the base\'s `en-US`', async () => { + expect(await listed()).toEqual([['en-US', BASE, SHIPPED_SUBJECT]]); + expect(await listed(BASE)).toEqual([['en-US', BASE, SHIPPED_SUBJECT]]); + expect(await executing()).toEqual([['en-US', BASE, SHIPPED_SUBJECT]]); + expect(await rendered('fr-FR')).toBe('✅ Task done: T'); + }); + + it('control: the pair\'s declared override of `en-US` reads as the override; the dependent\'s `fr-FR` reads as before', async () => { + const ensured = await call('POST', `/packages/${BASE}/ensure-customization`); + expect([200, 201], JSON.stringify(ensured.json)).toContain(ensured.status); + const saved = await saveInto(PAIR, body('en-US', OVERRIDE_SUBJECT, { overrides: { package: BASE, basedOn: '0.1.0' } })); + expect(saved.status, JSON.stringify(saved.json)).toBe(200); + const published = await publishFrom(PAIR, 'en-US'); + expect(published.status, JSON.stringify(published.json)).toBe(200); + + const OVERRIDE_EN = { status: 200, subject: OVERRIDE_SUBJECT, locale: 'en-US', _packageId: BASE }; + expect(await byName('en-US', BASE)).toEqual(OVERRIDE_EN); + expect(await byName('en-US')).toEqual(OVERRIDE_EN); + expect(await listed()).toEqual([['en-US', BASE, OVERRIDE_SUBJECT]]); + expect(await rendered('en-US')).toBe('Override: T'); + + expect(await byName('fr-FR', BASE)).toEqual(DEPENDENT_FR); + expect(await byName('fr-FR')).toEqual(DEPENDENT_FR); + expect(await rendered('fr-FR')).toBe('Override: T'); + }); +});