-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathbook.test.ts
More file actions
157 lines (144 loc) · 6.7 KB
/
Copy pathbook.test.ts
File metadata and controls
157 lines (144 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { describe, it, expect } from 'vitest';
import {
BookSchema,
resolveBookTree,
deriveImplicitPackageBook,
isPublicAudience,
type Book,
type ResolverDoc,
} from './book.zod';
const docs = (names: string[]): ResolverDoc[] => names.map((name) => ({ name }));
describe('BookSchema (ADR-0046 §6)', () => {
it('accepts a minimal spine', () => {
expect(() =>
BookSchema.parse({ name: 'crm_guide', groups: [{ key: 'start', label: 'Start', include: 'crm_*' }] }),
).not.toThrow();
});
it('rejects a non snake_case name', () => {
expect(() => BookSchema.parse({ name: 'CrmGuide', groups: [] })).toThrow();
});
it('rejects a non snake_case group key', () => {
expect(() => BookSchema.parse({ name: 'crm_guide', groups: [{ key: 'Start', label: 'x' }] })).toThrow();
});
it('accepts audience variants', () => {
for (const audience of ['org', 'public', { profile: 'admin' }] as const) {
expect(() => BookSchema.parse({ name: 'b', audience, groups: [] })).not.toThrow();
}
});
});
describe('resolveBookTree — derived membership (the AI-safety core)', () => {
it('derives membership by glob include, leaving docs untouched', () => {
const book: Book = {
name: 'crm',
groups: [
{ key: 'guides', label: 'Guides', include: 'crm_guide_*' },
{ key: 'ref', label: 'Reference', include: 'crm_ref_*' },
],
};
const tree = resolveBookTree(book, docs(['crm_guide_lead', 'crm_guide_deal', 'crm_ref_api']));
expect(tree.groups.map((g) => g.key)).toEqual(['guides', 'ref']);
expect(tree.groups[0].entries.map((e) => e.doc)).toEqual(['crm_guide_deal', 'crm_guide_lead']); // alpha
expect(tree.groups[1].entries.map((e) => e.doc)).toEqual(['crm_ref_api']);
});
it('a NEW doc matching a rule appears with zero edits to the book (create-and-forget)', () => {
const book: Book = { name: 'crm', groups: [{ key: 'guides', label: 'Guides', include: 'crm_guide_*' }] };
const before = resolveBookTree(book, docs(['crm_guide_lead']));
expect(before.groups[0].entries).toHaveLength(1);
// AI adds crm_guide_deal — same unchanged book spine:
const after = resolveBookTree(book, docs(['crm_guide_lead', 'crm_guide_deal']));
expect(after.groups[0].entries.map((e) => e.doc)).toEqual(['crm_guide_deal', 'crm_guide_lead']);
});
it('sorts within a group by doc.order then label', () => {
const book: Book = { name: 'crm', groups: [{ key: 'g', label: 'G', include: '*' }] };
const tree = resolveBookTree(book, [
{ name: 'b_doc', order: 2 },
{ name: 'a_doc', order: 1 },
{ name: 'z_doc' }, // order 0 → first
]);
expect(tree.groups[0].entries.map((e) => e.doc)).toEqual(['z_doc', 'a_doc', 'b_doc']);
});
it('honours explicit doc.group placement', () => {
const book: Book = {
name: 'crm',
groups: [
{ key: 'guides', label: 'Guides', include: 'never_*' },
{ key: 'admin', label: 'Admin' },
],
};
const tree = resolveBookTree(book, [{ name: 'setup', group: 'admin' }]);
expect(tree.groups.find((g) => g.key === 'admin')!.entries.map((e) => e.doc)).toEqual(['setup']);
});
it('routes unmatched docs to a synthetic Uncategorized group, never dropping them', () => {
const book: Book = { name: 'crm', groups: [{ key: 'guides', label: 'Guides', include: 'crm_guide_*' }] };
const tree = resolveBookTree(book, docs(['crm_guide_lead', 'crm_stray', 'crm_other']));
const unc = tree.groups.at(-1)!;
expect(unc.key).toBe('uncategorized');
expect(unc.entries.map((e) => e.doc)).toEqual(['crm_other', 'crm_stray']);
});
it('first group (by order) wins when two rules match the same doc — no duplicates', () => {
const book: Book = {
name: 'crm',
groups: [
{ key: 'second', label: 'Second', order: 2, include: 'crm_*' },
{ key: 'first', label: 'First', order: 1, include: 'crm_*' },
],
};
const tree = resolveBookTree(book, docs(['crm_x']));
expect(tree.groups.map((g) => g.key)).toEqual(['first', 'second']);
expect(tree.groups[0].entries.map((e) => e.doc)).toEqual(['crm_x']);
expect(tree.groups[1].entries).toHaveLength(0);
});
it('explicit pages override: verbatim order, separator, missing doc, and ... rest', () => {
const book: Book = {
name: 'crm',
groups: [
{
key: 'tut',
label: 'Tutorial',
include: 'crm_tut_*',
pages: ['crm_tut_intro', '---', 'crm_tut_missing', '...'],
},
],
};
const tree = resolveBookTree(book, docs(['crm_tut_intro', 'crm_tut_b', 'crm_tut_a']));
const e = tree.groups[0].entries;
expect(e[0]).toMatchObject({ doc: 'crm_tut_intro' });
expect(e[1]).toMatchObject({ separator: true });
expect(e[2]).toMatchObject({ doc: 'crm_tut_missing' }); // missing → renderer shows not-found
// '...' sweeps the rest of this group's matches, not the pinned intro:
expect(e.slice(3).map((x) => x.doc)).toEqual(['crm_tut_a', 'crm_tut_b']);
});
it('object node carries badge/icon and label override; href node is a link', () => {
const book: Book = {
name: 'crm',
groups: [{ key: 'g', label: 'G', pages: [{ doc: 'crm_api', badge: 'beta' }, { href: 'https://x', label: 'CHANGELOG' }] }],
};
const tree = resolveBookTree(book, [{ name: 'crm_api', label: 'API' }]);
expect(tree.groups[0].entries[0]).toMatchObject({ doc: 'crm_api', badge: 'beta', label: 'API' });
expect(tree.groups[0].entries[1]).toMatchObject({ href: 'https://x', label: 'CHANGELOG' });
});
it('include scoped by package ignores docs from other packages', () => {
const book: Book = { name: 'crm', groups: [{ key: 'g', label: 'G', include: '*', package: 'crm' }] };
const tree = resolveBookTree(book, [
{ name: 'a', packageId: 'crm' },
{ name: 'b', packageId: 'other' },
]);
expect(tree.groups[0].entries.map((e) => e.doc)).toEqual(['a']);
expect(tree.groups.at(-1)!.key).toBe('uncategorized'); // 'b' falls through
});
});
describe('deriveImplicitPackageBook + audience', () => {
it('synthesizes a one-group book including every doc of the package', () => {
const book = deriveImplicitPackageBook('app_todo', 'Todo');
const tree = resolveBookTree(book, docs(['todo_index', 'todo_guide']));
expect(tree.groups).toHaveLength(1);
expect(tree.groups[0].entries.map((e) => e.doc)).toEqual(['todo_guide', 'todo_index']);
});
it('isPublicAudience only true for public', () => {
expect(isPublicAudience('public')).toBe(true);
expect(isPublicAudience('org')).toBe(false);
expect(isPublicAudience({ profile: 'admin' })).toBe(false);
expect(isPublicAudience(undefined)).toBe(false);
});
});