mirror of https://github.com/garrytan/gstack.git
151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
/**
|
|
* gstack-core@1.0.0 schema pack validation (T1).
|
|
*
|
|
* Asserts the schema pack is well-formed and matches the v1.48 plan:
|
|
* - Exactly 8 page types (7 entities + 1 take)
|
|
* - Frontmatter shape is internally consistent
|
|
* - Retention policies match SKILL_RUN_RETENTION_DAYS spec
|
|
* - Link verbs only reference declared verbs
|
|
* - JSON payload shape is acceptable to mcp__gbrain__schema_apply_mutations
|
|
*
|
|
* Gate-tier, free, pure import + assertion.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
GSTACK_CORE_SCHEMA_PACK,
|
|
getSchemaPackMutationPayload,
|
|
getSchemaPackTypeNames,
|
|
getRetentionPolicy,
|
|
} from '../scripts/gstack-schema-pack';
|
|
import {
|
|
GSTACK_SCHEMA_PACK_NAME,
|
|
GSTACK_SCHEMA_PACK_VERSION,
|
|
} from '../scripts/brain-cache-spec';
|
|
|
|
describe('gstack-core schema pack', () => {
|
|
test('identity matches brain-cache-spec constants', () => {
|
|
expect(GSTACK_CORE_SCHEMA_PACK.name).toBe(GSTACK_SCHEMA_PACK_NAME);
|
|
expect(GSTACK_CORE_SCHEMA_PACK.version).toBe(GSTACK_SCHEMA_PACK_VERSION);
|
|
});
|
|
|
|
test('declares exactly 8 page types (7 entities + gstack/take)', () => {
|
|
expect(GSTACK_CORE_SCHEMA_PACK.page_types.length).toBe(8);
|
|
});
|
|
|
|
test('all 7 brain-cache entities have a matching schema page type', () => {
|
|
const types = getSchemaPackTypeNames();
|
|
const required = [
|
|
'gstack/user-profile',
|
|
'gstack/product',
|
|
'gstack/goal',
|
|
'gstack/developer-persona',
|
|
'gstack/brand',
|
|
'gstack/competitive-intel',
|
|
'gstack/skill-run',
|
|
];
|
|
for (const name of required) {
|
|
expect(types).toContain(name);
|
|
}
|
|
});
|
|
|
|
test('gstack/take exists with kind=bet supported (Phase 2 / E5)', () => {
|
|
const take = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/take');
|
|
expect(take).toBeDefined();
|
|
const kind = take!.fields.find((f) => f.name === 'kind');
|
|
expect(kind?.values).toContain('bet');
|
|
expect(kind?.values).toContain('fact');
|
|
});
|
|
|
|
test('every page type has a required type + slug field', () => {
|
|
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
|
const typeField = def.fields.find((f) => f.name === 'type');
|
|
const slugField = def.fields.find((f) => f.name === 'slug');
|
|
expect(typeField?.required).toBe(true);
|
|
expect(slugField?.required).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('enum fields declare their values', () => {
|
|
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
|
for (const field of def.fields) {
|
|
if (field.type === 'enum') {
|
|
expect(field.values).toBeDefined();
|
|
expect(field.values!.length).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('skill-run is the only archive-after-90d type', () => {
|
|
const archived = GSTACK_CORE_SCHEMA_PACK.page_types
|
|
.filter((t) => t.retention === 'archive-after-90d')
|
|
.map((t) => t.type);
|
|
expect(archived).toEqual(['gstack/skill-run']);
|
|
});
|
|
|
|
test('gstack/take is never-archive (calibration scorecard preservation)', () => {
|
|
expect(getRetentionPolicy('gstack/take')).toBe('never-archive');
|
|
});
|
|
|
|
test('getRetentionPolicy throws on unknown type (defensive)', () => {
|
|
expect(() => getRetentionPolicy('gstack/nonexistent')).toThrow();
|
|
});
|
|
|
|
test('link verbs declared on emits_links are also in pack.link_verbs', () => {
|
|
const declared = new Set(GSTACK_CORE_SCHEMA_PACK.link_verbs);
|
|
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
|
for (const link of def.emits_links ?? []) {
|
|
expect(declared.has(link.verb)).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('link verbs only target declared gstack/ page types', () => {
|
|
const declared = new Set(getSchemaPackTypeNames());
|
|
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
|
for (const link of def.emits_links ?? []) {
|
|
expect(declared.has(link.target_type)).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('mutation payload is well-formed JSON', () => {
|
|
const payload = getSchemaPackMutationPayload();
|
|
expect(payload.schema_version).toBe(1);
|
|
expect(payload.schema_pack).toBeDefined();
|
|
expect(typeof payload.schema_pack.name).toBe('string');
|
|
expect(Array.isArray(payload.schema_pack.page_types)).toBe(true);
|
|
// round-trip through JSON to catch unserializable values (functions, undefined, etc.)
|
|
const json = JSON.stringify(payload);
|
|
const reparsed = JSON.parse(json);
|
|
expect(reparsed.schema_pack.name).toBe(payload.schema_pack.name);
|
|
});
|
|
|
|
test('gstack/product has expected emits_links graph (product → goal/persona/brand/etc.)', () => {
|
|
const product = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/product')!;
|
|
const verbs = (product.emits_links ?? []).map((l) => `${l.verb}:${l.target_type}`);
|
|
expect(verbs).toContain('targets:gstack/goal');
|
|
expect(verbs).toContain('observed_by:gstack/developer-persona');
|
|
expect(verbs).toContain('has_brand:gstack/brand');
|
|
expect(verbs).toContain('competes_with:gstack/competitive-intel');
|
|
});
|
|
|
|
test('gstack/goal has lifecycle status enum (active/resolved/expired/archived)', () => {
|
|
const goal = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/goal')!;
|
|
const status = goal.fields.find((f) => f.name === 'status');
|
|
expect(status?.values).toEqual(['active', 'resolved', 'expired', 'archived']);
|
|
});
|
|
|
|
test('gstack/skill-run records the bet count for calibration coverage', () => {
|
|
const sr = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/skill-run')!;
|
|
const takesField = sr.fields.find((f) => f.name === 'takes_written');
|
|
expect(takesField).toBeDefined();
|
|
expect(takesField?.type).toBe('number');
|
|
});
|
|
|
|
test('gstack/user-profile is never-archive (cross-project, long-lived)', () => {
|
|
expect(getRetentionPolicy('gstack/user-profile')).toBe('never-archive');
|
|
});
|
|
});
|