feat: Phase 0 workspace, spikes, and golden harness
Stand up the monorepo skeleton and land the first Phase 0 artifacts: pairing threat model, plugin catalog, builder detect, 20-prompt golden harness, and synthetic 2GB mirror timing.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectBuilder } from '../src/builder-detect.ts';
|
||||
|
||||
describe('detectBuilder', () => {
|
||||
it('reports elementor when the plugin is active and post meta has _elementor_edit_mode', () => {
|
||||
expect(
|
||||
detectBuilder({
|
||||
theme: 'hello-elementor',
|
||||
plugins: [{ slug: 'elementor', active: true }],
|
||||
posts: [{ id: 1, slug: 'home', title: 'Home', content: '', meta: { _elementor_edit_mode: 'builder' } }],
|
||||
}),
|
||||
).toBe('elementor');
|
||||
});
|
||||
|
||||
it('reports beaver when beaver-builder is active and _fl_builder_data is present', () => {
|
||||
expect(
|
||||
detectBuilder({
|
||||
theme: 'bb-theme',
|
||||
plugins: [{ slug: 'beaver-builder-lite-version', active: true }],
|
||||
posts: [{ id: 1, slug: 'home', title: 'Home', content: '', meta: { _fl_builder_data: '{}' } }],
|
||||
}),
|
||||
).toBe('beaver');
|
||||
});
|
||||
|
||||
it('reports divi when the theme is Divi and _et_pb_use_builder is on', () => {
|
||||
expect(
|
||||
detectBuilder({
|
||||
theme: 'Divi',
|
||||
plugins: [],
|
||||
posts: [{ id: 1, slug: 'home', title: 'Home', content: '', meta: { _et_pb_use_builder: 'on' } }],
|
||||
}),
|
||||
).toBe('divi');
|
||||
});
|
||||
|
||||
it('reports gutenberg when content has block markup and no builder plugin', () => {
|
||||
expect(
|
||||
detectBuilder({
|
||||
theme: 'twentytwentyfour',
|
||||
plugins: [],
|
||||
posts: [{ id: 1, slug: 'home', title: 'Home', content: '<!-- wp:heading --><h1>Hi</h1><!-- /wp:heading -->', meta: {} }],
|
||||
}),
|
||||
).toBe('gutenberg');
|
||||
});
|
||||
|
||||
it('reports classic when content is HTML and no builder plugin is active', () => {
|
||||
expect(
|
||||
detectBuilder({
|
||||
theme: 'twentytwentyone',
|
||||
plugins: [],
|
||||
posts: [{ id: 1, slug: 'home', title: 'Home', content: '<h1>Hi</h1>', meta: {} }],
|
||||
}),
|
||||
).toBe('classic');
|
||||
});
|
||||
|
||||
it('prefers elementor over gutenberg markup when both are present', () => {
|
||||
expect(
|
||||
detectBuilder({
|
||||
theme: 'hello-elementor',
|
||||
plugins: [{ slug: 'elementor', active: true }],
|
||||
posts: [
|
||||
{
|
||||
id: 1,
|
||||
slug: 'home',
|
||||
title: 'Home',
|
||||
content: '<!-- wp:heading --><h1>Hi</h1><!-- /wp:heading -->',
|
||||
meta: { _elementor_data: '[]' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe('elementor');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { exportDbSubset } from '../src/subset.ts';
|
||||
import { mediaProxyTarget, stageReplacement } from '../src/media-proxy.ts';
|
||||
import type { SiteExport } from '../src/types.ts';
|
||||
|
||||
const dump = (): SiteExport => ({
|
||||
origin: 'https://example.com',
|
||||
tables: {
|
||||
wp_posts: [{ ID: 1, post_title: 'Home' }],
|
||||
wp_postmeta: [{ post_id: 1, meta_key: '_edit_lock', meta_value: '1' }],
|
||||
wp_options: [
|
||||
{ option_name: 'blogname', option_value: 'Biz' },
|
||||
{ option_name: 'woocommerce_stripe_secret_key', option_value: 'sk_live_xxx' },
|
||||
{ option_name: 'smtp_pass', option_value: 'secret' },
|
||||
],
|
||||
wp_wc_orders: [{ id: 99, total: '40.00' }],
|
||||
wp_comments: [{ comment_ID: 1, comment_content: 'hi' }],
|
||||
},
|
||||
uploads: [{ path: '/wp-content/uploads/2024/hero.jpg', bytes: 2_147_483_648 }],
|
||||
});
|
||||
|
||||
describe('exportDbSubset', () => {
|
||||
it('keeps posts, postmeta, and options for a content playbook', () => {
|
||||
expect(exportDbSubset(dump(), { playbook: 'content', postIds: [1] }).tables).toEqual(
|
||||
expect.arrayContaining(['wp_posts', 'wp_postmeta', 'wp_options']),
|
||||
);
|
||||
});
|
||||
|
||||
it('drops Woo orders from a content-edit slice', () => {
|
||||
expect(exportDbSubset(dump(), { playbook: 'content', postIds: [1] }).tables).not.toContain('wp_wc_orders');
|
||||
});
|
||||
|
||||
it('drops comments from a content-edit slice', () => {
|
||||
expect(exportDbSubset(dump(), { playbook: 'content', postIds: [1] }).tables).not.toContain('wp_comments');
|
||||
});
|
||||
|
||||
it('redacts option names ending in _key, _secret, or smtp_pass', () => {
|
||||
const options = exportDbSubset(dump(), { playbook: 'content', postIds: [1] }).options;
|
||||
expect(options.every((name) => !/(_key|_secret|smtp_pass)$/.test(name))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('media proxy', () => {
|
||||
it('does not copy the uploads library', () => {
|
||||
expect(mediaProxyTarget(dump(), '/wp-content/uploads/2024/hero.jpg')).toBe(
|
||||
'https://example.com/wp-content/uploads/2024/hero.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
it('copies a file only when it is replaced', () => {
|
||||
const staged = stageReplacement(dump(), '/wp-content/uploads/2024/hero.jpg', 12);
|
||||
expect(staged.copiedPaths).toEqual(['/wp-content/uploads/2024/hero.jpg']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { loadPrompts } from '../src/load-prompts.ts';
|
||||
import { loadSite } from '../src/load-site.ts';
|
||||
|
||||
describe('golden prompts', () => {
|
||||
it('loads twenty prompts', () => {
|
||||
expect(loadPrompts()).toHaveLength(20);
|
||||
});
|
||||
|
||||
it('covers at least two site fixtures', () => {
|
||||
expect(new Set(loadPrompts().map((p) => p.site)).size).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('gives every prompt a preview_text, option, or screenshot assertion', () => {
|
||||
const types = new Set(loadPrompts().map((p) => p.assert.type));
|
||||
expect([...types].every((t) => t === 'preview_text' || t === 'option' || t === 'screenshot')).toBe(true);
|
||||
});
|
||||
|
||||
it('points every prompt at a site fixture that exists', () => {
|
||||
expect(loadPrompts().every((p) => loadSite(p.site).id === p.site)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { applyTool } from '../src/apply-tool.ts';
|
||||
import { checkAssertion } from '../src/check-assertion.ts';
|
||||
import { scoreGrokResponse } from '../src/score.ts';
|
||||
import type { SiteFixture } from '../src/types.ts';
|
||||
|
||||
const site = (): SiteFixture => ({
|
||||
id: 'gutenberg-business',
|
||||
theme: 'twentytwentyfour',
|
||||
plugins: [],
|
||||
wordpressVersion: '6.6',
|
||||
phpVersion: '8.2',
|
||||
options: { blogname: 'Old Biz', blog_public: '1' },
|
||||
posts: [
|
||||
{
|
||||
id: 1,
|
||||
slug: 'homepage',
|
||||
title: 'Home',
|
||||
content: '<!-- wp:heading --><h1>Welcome to our site</h1><!-- /wp:heading -->',
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
uploads: [],
|
||||
});
|
||||
|
||||
describe('applyTool', () => {
|
||||
it('replaces the homepage heading', () => {
|
||||
const next = applyTool(site(), {
|
||||
name: 'edit_heading',
|
||||
arguments: { page: 'homepage', newText: 'Welcome to My Business' },
|
||||
});
|
||||
expect(next.posts[0]?.content).toContain('Welcome to My Business');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAssertion', () => {
|
||||
it('passes preview_text when the page contains the string', () => {
|
||||
const next = applyTool(site(), {
|
||||
name: 'edit_heading',
|
||||
arguments: { page: 'homepage', newText: 'Welcome to My Business' },
|
||||
});
|
||||
expect(
|
||||
checkAssertion(next, { type: 'preview_text', page: 'homepage', contains: 'Welcome to My Business' }).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('fails preview_text when the string is absent', () => {
|
||||
expect(
|
||||
checkAssertion(site(), { type: 'preview_text', page: 'homepage', contains: 'Welcome to My Business' }).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('passes option when the value matches', () => {
|
||||
const next = applyTool(site(), {
|
||||
name: 'update_option',
|
||||
arguments: { key: 'blogname', value: 'My Business' },
|
||||
});
|
||||
expect(checkAssertion(next, { type: 'option', key: 'blogname', value: 'My Business' }).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scoreGrokResponse', () => {
|
||||
it('passes a Grok tool-call that satisfies the assertion', () => {
|
||||
const verdict = scoreGrokResponse({
|
||||
site: site(),
|
||||
assert: { type: 'preview_text', page: 'homepage', contains: 'Welcome to My Business' },
|
||||
grok: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
name: 'edit_heading',
|
||||
arguments: JSON.stringify({ page: 'homepage', newText: 'Welcome to My Business' }),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(verdict.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user