ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
4.4 KiB
TypeScript
96 lines
4.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { seedCompany } from '@/tests/pg/fixtures'
|
|
import { withUserContext } from '@/tests/pg/setup'
|
|
|
|
describe('agent_atom_registry.pg: seed + RLS', () => {
|
|
it('seed migration populated active atoms with non-null bodies and no swarm-* ids', async () => {
|
|
const { userId } = await seedCompany()
|
|
const rows = await withUserContext(userId, async (client) => {
|
|
const res = await client.query<{ id: string; body: string | null; mcp_exposed: boolean }>(
|
|
`SELECT id, body, mcp_exposed FROM public.agent_atom_registry WHERE is_active`,
|
|
)
|
|
return res.rows
|
|
})
|
|
|
|
// The generated seed migration (…_seed_agent_atom_bodies.sql) runs during replay.
|
|
expect(rows.length).toBeGreaterThan(0)
|
|
// Every active atom has a real body inlined: the production read path depends
|
|
// on this (no disk fallback in prod).
|
|
for (const r of rows) {
|
|
expect(r.body, `atom ${r.id} body should be non-empty`).toBeTruthy()
|
|
expect(r.mcp_exposed).toBe(true)
|
|
}
|
|
// swarm-* audit skills must never become atoms; a curated horizontal must be present.
|
|
expect(rows.some((r) => r.id.includes('swarm'))).toBe(false)
|
|
expect(rows.some((r) => r.id === 'horizontal/swedish-vat')).toBe(true)
|
|
})
|
|
|
|
it('seeds reference children whose parent_atom_id points at a real top-level skill', async () => {
|
|
const { userId } = await seedCompany()
|
|
const { children, orphans, vatRef } = await withUserContext(userId, async (client) => {
|
|
const childRes = await client.query<{ n: number }>(
|
|
`SELECT count(*)::int AS n FROM public.agent_atom_registry WHERE parent_atom_id IS NOT NULL`,
|
|
)
|
|
// FK already guarantees this, but assert there are no dangling links anyway.
|
|
const orphanRes = await client.query<{ n: number }>(
|
|
`SELECT count(*)::int AS n
|
|
FROM public.agent_atom_registry c
|
|
WHERE c.parent_atom_id IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM public.agent_atom_registry p WHERE p.id = c.parent_atom_id
|
|
)`,
|
|
)
|
|
const vatRefRes = await client.query<{ id: string; body: string | null; parent_atom_id: string }>(
|
|
`SELECT id, body, parent_atom_id FROM public.agent_atom_registry
|
|
WHERE id = 'horizontal/swedish-vat/vat-compliance-reference'`,
|
|
)
|
|
return { children: childRes.rows[0]!.n, orphans: orphanRes.rows[0]!.n, vatRef: vatRefRes.rows[0] }
|
|
})
|
|
|
|
expect(children).toBeGreaterThan(0)
|
|
expect(orphans).toBe(0)
|
|
expect(vatRef?.parent_atom_id).toBe('horizontal/swedish-vat')
|
|
expect(vatRef?.body).toBeTruthy()
|
|
})
|
|
|
|
it('catalog filter (parent_atom_id IS NULL) hides references but the parent footer exposes them', async () => {
|
|
const { userId } = await seedCompany()
|
|
const { listed, vatBody } = await withUserContext(userId, async (client) => {
|
|
// The query every catalog uses: top-level skills only.
|
|
const listedRes = await client.query<{ id: string }>(
|
|
`SELECT id FROM public.agent_atom_registry
|
|
WHERE is_active AND parent_atom_id IS NULL`,
|
|
)
|
|
const vatRes = await client.query<{ body: string }>(
|
|
`SELECT body FROM public.agent_atom_registry WHERE id = 'horizontal/swedish-vat'`,
|
|
)
|
|
return { listed: listedRes.rows.map((r) => r.id), vatBody: vatRes.rows[0]?.body ?? '' }
|
|
})
|
|
|
|
// No reference id (three-segment, "<tier>/<slug>/<ref>") survives the filter.
|
|
expect(listed.some((id) => id.split('/').length === 3)).toBe(false)
|
|
expect(listed).toContain('horizontal/swedish-vat')
|
|
// …yet the loadable id is reachable from the parent body once loaded.
|
|
expect(vatBody).toContain('gnubok_load_skill("horizontal/swedish-vat/vat-compliance-reference")')
|
|
})
|
|
|
|
it('authenticated users can read the catalog but not write it', async () => {
|
|
const { userId } = await seedCompany()
|
|
await withUserContext(userId, async (client) => {
|
|
const sel = await client.query<{ n: number }>(
|
|
`SELECT count(*)::int AS n FROM public.agent_atom_registry`,
|
|
)
|
|
expect(sel.rows[0]!.n).toBeGreaterThan(0)
|
|
|
|
// No INSERT policy for authenticated: the catalog ships via service-role
|
|
// migrations only.
|
|
await expect(
|
|
client.query(
|
|
`INSERT INTO public.agent_atom_registry (id, tier, title, description, body_path)
|
|
VALUES ('horizontal/hacker', 'horizontal', 'x', 'x', 'x')`,
|
|
),
|
|
).rejects.toThrow()
|
|
})
|
|
})
|
|
})
|