Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
238 lines
6.9 KiB
TypeScript
238 lines
6.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
COMPOSE_TEXT_ACTION_ID,
|
|
COMPOSE_TEXT_VERSION,
|
|
composeTextOutputsSchema,
|
|
generateComposeTextStableKey,
|
|
renderComposeTextOutputs,
|
|
renderTemplateDocumentToMarkdown,
|
|
templateDocumentSchema,
|
|
validateComposeTextConfig,
|
|
} from '../composeText';
|
|
|
|
describe('composeText helpers', () => {
|
|
it('T002: validates compose-text config outputs and rejects missing outputs', () => {
|
|
const valid = validateComposeTextConfig({
|
|
actionId: COMPOSE_TEXT_ACTION_ID,
|
|
version: COMPOSE_TEXT_VERSION,
|
|
outputs: [
|
|
{
|
|
id: 'out-1',
|
|
label: 'Prompt',
|
|
stableKey: 'prompt',
|
|
document: {
|
|
version: 1,
|
|
blocks: [{ type: 'paragraph', children: [{ type: 'text', text: 'Hello' }] }],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const invalid = validateComposeTextConfig({
|
|
actionId: COMPOSE_TEXT_ACTION_ID,
|
|
version: COMPOSE_TEXT_VERSION,
|
|
});
|
|
|
|
expect(valid).toMatchObject({ ok: true });
|
|
expect(invalid).toMatchObject({ ok: false });
|
|
if (invalid.ok === false) {
|
|
expect(invalid.errors[0]).toContain('at least one output');
|
|
}
|
|
});
|
|
|
|
it('T003/T004: generates stable keys from freeform labels and disambiguates collisions', () => {
|
|
expect(generateComposeTextStableKey('Prompt Body')).toBe('prompt_body');
|
|
expect(generateComposeTextStableKey('123 Summary')).toBe('output_123_summary');
|
|
expect(generateComposeTextStableKey('Prompt Body', ['prompt_body'])).toBe('prompt_body_2');
|
|
});
|
|
|
|
it('T006/T007/T008: rejects empty labels plus duplicate labels and stable keys', () => {
|
|
const parsed = composeTextOutputsSchema.safeParse([
|
|
{
|
|
id: 'out-1',
|
|
label: ' ',
|
|
stableKey: 'prompt',
|
|
document: { version: 1, blocks: [] },
|
|
},
|
|
{
|
|
id: 'out-2',
|
|
label: 'Prompt',
|
|
stableKey: 'prompt',
|
|
document: { version: 1, blocks: [] },
|
|
},
|
|
{
|
|
id: 'out-3',
|
|
label: 'prompt',
|
|
stableKey: 'prompt_2',
|
|
document: { version: 1, blocks: [] },
|
|
},
|
|
]);
|
|
|
|
expect(parsed.success).toBe(false);
|
|
expect(parsed.success ? [] : parsed.error.issues.map((issue) => issue.message)).toEqual(
|
|
expect.arrayContaining([
|
|
'String must contain at least 1 character(s)',
|
|
'Output labels must be unique within the step.',
|
|
'Stable keys must be unique within the step.',
|
|
])
|
|
);
|
|
});
|
|
|
|
it('T009/T010: restricts template documents to markdown-safe nodes with simple references only', () => {
|
|
const valid = templateDocumentSchema.safeParse({
|
|
version: 1,
|
|
blocks: [
|
|
{
|
|
type: 'paragraph',
|
|
children: [
|
|
{ type: 'text', text: 'Hello', marks: ['bold'] },
|
|
{ type: 'reference', path: 'payload.ticket.id', label: 'Ticket ID' },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
const invalidBlock = templateDocumentSchema.safeParse({
|
|
version: 1,
|
|
blocks: [{ type: 'table', children: [] }],
|
|
});
|
|
const invalidReference = templateDocumentSchema.safeParse({
|
|
version: 1,
|
|
blocks: [
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ type: 'reference', path: 'payload.ticket.id & payload.name', label: 'Bad' }],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(valid.success).toBe(true);
|
|
expect(invalidBlock.success).toBe(false);
|
|
expect(invalidReference.success).toBe(false);
|
|
});
|
|
|
|
it('T014/T015/T016: renders paragraphs, hard breaks, lists, and inline formatting as markdown', async () => {
|
|
const markdown = await renderTemplateDocumentToMarkdown(
|
|
{
|
|
version: 1,
|
|
blocks: [
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ type: 'text', text: 'Line 1\nLine 2' }],
|
|
},
|
|
{
|
|
type: 'bullet_list_item',
|
|
children: [{ type: 'text', text: 'One', marks: ['bold'] }],
|
|
},
|
|
{
|
|
type: 'bullet_list_item',
|
|
children: [{ type: 'text', text: 'Two', marks: ['italic', 'code'] }],
|
|
},
|
|
{
|
|
type: 'ordered_list_item',
|
|
children: [{ type: 'text', text: 'Three', marks: ['link'], href: 'https://example.com' }],
|
|
},
|
|
],
|
|
},
|
|
{ outputKey: 'prompt' }
|
|
);
|
|
|
|
expect(markdown).toBe(
|
|
'Line 1 \nLine 2\n\n- **One**\n- _`Two`_\n\n1. [Three](https://example.com)'
|
|
);
|
|
});
|
|
|
|
it('T017/T018/T019/T020/T021: renders multiple outputs from payload and vars references and fails on missing references', async () => {
|
|
const config = {
|
|
actionId: COMPOSE_TEXT_ACTION_ID,
|
|
version: COMPOSE_TEXT_VERSION,
|
|
outputs: [
|
|
{
|
|
id: 'out-1',
|
|
label: 'Prompt',
|
|
stableKey: 'prompt',
|
|
document: {
|
|
version: 1,
|
|
blocks: [
|
|
{
|
|
type: 'paragraph',
|
|
children: [
|
|
{ type: 'text', text: 'Ticket ' },
|
|
{ type: 'reference', path: 'payload.ticket.id', label: 'Ticket ID' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: 'out-2',
|
|
label: 'Summary',
|
|
stableKey: 'summary',
|
|
document: {
|
|
version: 1,
|
|
blocks: [
|
|
{
|
|
type: 'paragraph',
|
|
children: [
|
|
{ type: 'reference', path: 'vars.ticketResult.updated', label: 'Updated' },
|
|
{ type: 'text', text: ' / ' },
|
|
{ type: 'reference', path: 'vars.ticketResult.ticket_id', label: 'Ticket ID' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const rendered = await renderComposeTextOutputs(config, {
|
|
payload: { ticket: { id: 'T-100' } },
|
|
vars: { ticketResult: { updated: true, ticket_id: 'T-100' } },
|
|
meta: {},
|
|
error: undefined,
|
|
});
|
|
|
|
expect(rendered).toEqual({
|
|
prompt: 'Ticket T-100',
|
|
summary: 'true / T-100',
|
|
});
|
|
|
|
await expect(
|
|
renderComposeTextOutputs(
|
|
{
|
|
...config,
|
|
outputs: [
|
|
...config.outputs,
|
|
{
|
|
id: 'out-3',
|
|
label: 'Missing',
|
|
stableKey: 'missing',
|
|
document: {
|
|
version: 1,
|
|
blocks: [
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ type: 'reference', path: 'vars.ticketResult.subject', label: 'Subject' }],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
payload: { ticket: { id: 'T-100' } },
|
|
vars: { ticketResult: { updated: true, ticket_id: 'T-100' } },
|
|
meta: {},
|
|
error: undefined,
|
|
}
|
|
)
|
|
).rejects.toMatchObject({
|
|
category: 'ValidationError',
|
|
code: 'MISSING_REFERENCE',
|
|
details: {
|
|
outputKey: 'missing',
|
|
referencePath: 'vars.ticketResult.subject',
|
|
},
|
|
});
|
|
});
|
|
});
|