Compare commits
2 Commits
feat/onboa
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182fb2357f | ||
| b1d3686d35 |
@@ -9,6 +9,7 @@ import {
|
||||
customTopicSchema,
|
||||
graphActionsSchema,
|
||||
proposeGraphDeltaSchema,
|
||||
onboardingOverviewSchema,
|
||||
} from '../llmSchemas';
|
||||
|
||||
const sampleTopic = {
|
||||
@@ -192,3 +193,56 @@ describe('proposeGraphDeltaSchema', () => {
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onboardingOverviewSchema (issue #32 — fast-tier stringified arrays)', () => {
|
||||
const base = {
|
||||
title: 'Privacy',
|
||||
what_it_is: 'How we handle data.',
|
||||
why_it_matters: 'You touch personal data weekly.',
|
||||
};
|
||||
const points = ['Point one', 'Point two', 'Point three'];
|
||||
const topics = [{ topic_id: 'avg', label: 'AVG' }];
|
||||
|
||||
it('accepts well-formed output', () => {
|
||||
const r = onboardingOverviewSchema.parse({ ...base, key_points: points, topics_covered: topics });
|
||||
expect(r.key_points).toEqual(points);
|
||||
expect(r.topics_covered).toEqual(topics);
|
||||
});
|
||||
|
||||
it('coerces JSON-stringified arrays back to arrays (the exact #32 failure)', () => {
|
||||
const r = onboardingOverviewSchema.parse({
|
||||
...base,
|
||||
key_points: JSON.stringify(points),
|
||||
topics_covered: JSON.stringify(topics),
|
||||
});
|
||||
expect(r.key_points).toEqual(points);
|
||||
expect(r.topics_covered).toEqual(topics);
|
||||
});
|
||||
|
||||
it('splits a bullet/newline string of key points as a fallback', () => {
|
||||
const r = onboardingOverviewSchema.parse({
|
||||
...base,
|
||||
key_points: '- Point one\n• Point two\n3. Point three',
|
||||
topics_covered: topics,
|
||||
});
|
||||
expect(r.key_points).toEqual(points);
|
||||
});
|
||||
|
||||
it('keeps the first 5 key points instead of failing on overage', () => {
|
||||
const seven = Array.from({ length: 7 }, (_, i) => `P${i + 1}`);
|
||||
const r = onboardingOverviewSchema.parse({ ...base, key_points: seven, topics_covered: topics });
|
||||
expect(r.key_points).toEqual(['P1', 'P2', 'P3', 'P4', 'P5']);
|
||||
});
|
||||
|
||||
it('still rejects genuinely bad output', () => {
|
||||
expect(() =>
|
||||
onboardingOverviewSchema.parse({ ...base, key_points: ['only', 'two'], topics_covered: topics }),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
onboardingOverviewSchema.parse({ ...base, key_points: points, topics_covered: 'not json at all' }),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
onboardingOverviewSchema.parse({ ...base, key_points: 42, topics_covered: topics }),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,14 +229,43 @@ export const themeSessionSchema = z.object({
|
||||
keyTakeaways: z.array(z.string().min(1)).min(3),
|
||||
});
|
||||
|
||||
// Fast-tier models occasionally emit array tool fields as a JSON-encoded
|
||||
// string ("[\"a\",\"b\"]") instead of a real array (issue #32). Parse those
|
||||
// back to arrays before validating; leave anything else untouched so real
|
||||
// type errors still fail validation.
|
||||
function coerceStringifiedArray(v) {
|
||||
if (typeof v === 'string') {
|
||||
const s = v.trim();
|
||||
if (s.startsWith('[')) {
|
||||
try { return JSON.parse(s); } catch { /* keep original, let Zod report */ }
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const onboardingKeyPoints = z.preprocess((v) => {
|
||||
let out = coerceStringifiedArray(v);
|
||||
// Fallback: a bullet/newline list as one string → split into points.
|
||||
if (typeof out === 'string' && out.includes('\n')) {
|
||||
out = out
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^\s*[-•*\d.]+\s*/, '').trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
// Overage is trivial — keep the first 5 rather than failing the user.
|
||||
if (Array.isArray(out) && out.length > 5) out = out.slice(0, 5);
|
||||
return out;
|
||||
}, z.array(z.string().min(1)).min(3).max(5));
|
||||
|
||||
export const onboardingOverviewSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
what_it_is: z.string().min(1),
|
||||
why_it_matters: z.string().min(1),
|
||||
key_points: z.array(z.string().min(1)).min(3).max(5),
|
||||
topics_covered: z
|
||||
.array(z.object({ topic_id: z.string().min(1), label: z.string().min(1) }))
|
||||
.min(1),
|
||||
key_points: onboardingKeyPoints,
|
||||
topics_covered: z.preprocess(
|
||||
coerceStringifiedArray,
|
||||
z.array(z.object({ topic_id: z.string().min(1), label: z.string().min(1) })).min(1),
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -470,11 +470,11 @@ export const EMIT_ONBOARDING_OVERVIEW_TOOL = {
|
||||
items: { type: 'string' },
|
||||
minItems: 3,
|
||||
maxItems: 5,
|
||||
description: '3–5 short, concrete takeaways a newcomer should remember about this theme.',
|
||||
description: '3–5 short, concrete takeaways a newcomer should remember about this theme. Must be a JSON array of strings — never a single string.',
|
||||
},
|
||||
topics_covered: {
|
||||
type: 'array',
|
||||
description: 'The topics that make up this theme. Reuse the exact topic_id you were given so the UI can link back.',
|
||||
description: 'The topics that make up this theme. Reuse the exact topic_id you were given so the UI can link back. Must be a JSON array of objects — never a string.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
Reference in New Issue
Block a user