Compare commits
1 Commits
b1d3686d35
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182fb2357f |
@@ -9,6 +9,7 @@ import {
|
|||||||
customTopicSchema,
|
customTopicSchema,
|
||||||
graphActionsSchema,
|
graphActionsSchema,
|
||||||
proposeGraphDeltaSchema,
|
proposeGraphDeltaSchema,
|
||||||
|
onboardingOverviewSchema,
|
||||||
} from '../llmSchemas';
|
} from '../llmSchemas';
|
||||||
|
|
||||||
const sampleTopic = {
|
const sampleTopic = {
|
||||||
@@ -192,3 +193,56 @@ describe('proposeGraphDeltaSchema', () => {
|
|||||||
).toThrow();
|
).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),
|
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({
|
export const onboardingOverviewSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
what_it_is: z.string().min(1),
|
what_it_is: z.string().min(1),
|
||||||
why_it_matters: z.string().min(1),
|
why_it_matters: z.string().min(1),
|
||||||
key_points: z.array(z.string().min(1)).min(3).max(5),
|
key_points: onboardingKeyPoints,
|
||||||
topics_covered: z
|
topics_covered: z.preprocess(
|
||||||
.array(z.object({ topic_id: z.string().min(1), label: z.string().min(1) }))
|
coerceStringifiedArray,
|
||||||
.min(1),
|
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' },
|
items: { type: 'string' },
|
||||||
minItems: 3,
|
minItems: 3,
|
||||||
maxItems: 5,
|
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: {
|
topics_covered: {
|
||||||
type: 'array',
|
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: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
Reference in New Issue
Block a user