Files
gbrain/test/post-install-advisory.test.ts
T
9d88680a51 v0.42.47.0 feat(skillpack,advisor): brain-resident skillpacks + proactive gbrain advisor (#2180) (#2231)
* feat(skillpack): brain_resident manifest fields + init-brain-pack scaffolder + tools version-skew lint

Add optional brain_resident/schema_pack to the v1 manifest (additive,
forward-compatible). New runInitBrainPack scaffolds a brain-resident pack
(brain_resident:true, exact gbrain_min_version, 5-section machine-parseable
README) beside brain content; applyWritePlan factored out of init-scaffold.
brain-pack-lint validates each skill's declared tools: against the serving op
set (E6 version-skew). Wires gbrain skillpack init-brain-pack.

* feat(skillpack): Topology A brain-pack discovery on sources add + bounded nag

After 'gbrain sources add', if the source ships a brain_resident pack, print an
agent-readable advisory (ask the user before scaffolding). nag-state.ts tracks
declines per (source-repo brain_id, source, pack) with escalate-then-suppress;
declines count only on CLI-interactive displays, never cron/MCP. Fail-open: a
malformed/absent pack never breaks sources add.

* feat(advisor,skillpack): list_brain_skillpack MCP tool + gbrain advisor

Topology B: dedicated source-scoped list_brain_skillpack op + get_skill source_id
disambiguation (brain-resident-locate.ts); git scaffold-spec never a server FS
path; source-aware schema match. LEARN_INSTRUCTION + serve-http banner.

gbrain advisor: read-only ranked actions from brain state (8 resilient collectors,
shared renderer, JSONL history, --json severity exit codes, local-only argv
--apply dispatcher). Exposed over MCP behind mcp.publish_advisor (default off,
read-only on remote; workspace collectors no-op remotely). Generalizes
post-install-advisory to a single current-state recommended set (install→scaffold).

* feat(skills): bundle gbrain-advisor skill + weekly cron recipe + ranking eval

skills/gbrain-advisor teaches a harness to run gbrain advisor on a cadence and
ping the user (read-only; ask before fixing). Registered in manifest.json,
RESOLVER.md, openclaw.plugin.json. E4 ranking-precision eval on seeded-defect
fixtures (100%).

* chore: bump version and changelog (v0.42.47.0)

Brain-resident skillpacks + gbrain advisor (#2180).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: sync CLAUDE.md + KEY_FILES for brain-resident skillpacks + advisor (#2180)

Skill count 29->30, Skills section gains the brain-resident skillpacks + advisor
capability, KEY_FILES gets current-state entries for the new modules. Regenerate
llms bundles.

* test,fix: align stale assertions with generalized advisory + advisor resolver triggers (#2180)

- post-install-advisory.test.ts: install→scaffold wording (the install verb was
  removed); restore two-column in book-mirror copy; drop the removed skillpack-list line.
- RESOLVER.md: gbrain-advisor trigger now fuzzy-matches a declared frontmatter
  trigger (resolver round-trip D5/C).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: regenerate llms bundle for updated advisor resolver row (#2180)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:45:22 -07:00

234 lines
7.4 KiB
TypeScript

/**
* Tests for src/core/skillpack/post-install-advisory.ts (v0.25.1).
*
* The advisory is meant to be read by the agent (openclaw, claude-code)
* from the terminal output of `gbrain init` and `gbrain post-upgrade`.
* These tests pin:
* - empty/no-workspace path renders a workspace-detection note
* - all-installed path returns null (no-op)
* - partial-install path lists ONLY the missing skills
* - the rendered text contains the explicit "ASK THE USER FIRST"
* framing so future changes to the prose can't accidentally
* drop the user-sovereignty contract
*/
import { describe, expect, it, afterEach } from 'bun:test';
import {
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
buildAdvisory,
detectInstalledSlugs,
} from '../src/core/skillpack/post-install-advisory.ts';
const cleanup: string[] = [];
afterEach(() => {
while (cleanup.length) {
const d = cleanup.pop();
if (d && existsSync(d)) rmSync(d, { recursive: true, force: true });
}
});
function scratchWorkspace(receiptSlugs: string[] | null): {
workspace: string;
skillsDir: string;
} {
const workspace = mkdtempSync(join(tmpdir(), 'advisory-test-'));
cleanup.push(workspace);
const skillsDir = join(workspace, 'skills');
mkdirSync(skillsDir, { recursive: true });
if (receiptSlugs === null) {
writeFileSync(
join(skillsDir, 'RESOLVER.md'),
'# RESOLVER\n\n| Trigger | Skill |\n|---------|-------|\n',
);
} else {
const slugList = receiptSlugs.sort().join(',');
const block = [
'# RESOLVER',
'',
'| Trigger | Skill |',
'|---------|-------|',
'',
'<!-- gbrain:skillpack:begin -->',
`<!-- gbrain:skillpack:manifest cumulative-slugs="${slugList}" version="0.25.1" -->`,
...receiptSlugs.map(
(s) => `| "${s}" | \`skills/${s}/SKILL.md\` |`,
),
'<!-- gbrain:skillpack:end -->',
'',
].join('\n');
writeFileSync(join(skillsDir, 'RESOLVER.md'), block);
}
return { workspace, skillsDir };
}
describe('detectInstalledSlugs', () => {
it('returns empty set when no managed block', () => {
const { workspace, skillsDir } = scratchWorkspace(null);
expect(detectInstalledSlugs(skillsDir, workspace).size).toBe(0);
});
it('reads cumulative-slugs receipt', () => {
const { workspace, skillsDir } = scratchWorkspace([
'brain-ops',
'idea-ingest',
]);
const set = detectInstalledSlugs(skillsDir, workspace);
expect(set.has('brain-ops')).toBe(true);
expect(set.has('idea-ingest')).toBe(true);
expect(set.size).toBe(2);
});
});
describe('buildAdvisory — partial-install path', () => {
it('lists ONLY missing skills when most are already installed', () => {
const { workspace, skillsDir } = scratchWorkspace([
'brain-ops',
'article-enrichment',
'strategic-reading',
'concept-synthesis',
'perplexity-research',
'archive-crawler',
'academic-verify',
'brain-pdf',
'voice-note-ingest',
]);
const advisory = buildAdvisory({
version: '0.25.1',
context: 'upgrade',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
});
expect(advisory).not.toBeNull();
expect(advisory).toContain('book-mirror');
expect(advisory).not.toContain('article-enrichment');
expect(advisory).not.toContain('strategic-reading');
expect(advisory).toContain('gbrain skillpack scaffold book-mirror');
});
it('uses --all command when ALL recommended are missing (fresh workspace)', () => {
const { workspace, skillsDir } = scratchWorkspace([]);
const advisory = buildAdvisory({
version: '0.25.1',
context: 'init',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
});
expect(advisory).not.toBeNull();
expect(advisory).toContain('gbrain skillpack scaffold --all');
expect(advisory).toContain('book-mirror');
expect(advisory).toContain('article-enrichment');
expect(advisory).toContain('strategic-reading');
});
});
describe('buildAdvisory — all-installed → null (no nag)', () => {
it('returns null when every recommended skill is already installed', () => {
const allRecommended = [
'book-mirror',
'article-enrichment',
'strategic-reading',
'concept-synthesis',
'perplexity-research',
'archive-crawler',
'academic-verify',
'brain-pdf',
'voice-note-ingest',
];
const { workspace, skillsDir } = scratchWorkspace(allRecommended);
const advisory = buildAdvisory({
version: '0.25.1',
context: 'upgrade',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
});
expect(advisory).toBeNull();
});
});
describe('buildAdvisory — agent-readable framing', () => {
it('contains the user-sovereignty contract phrasing', () => {
const { workspace, skillsDir } = scratchWorkspace([]);
const advisory = buildAdvisory({
version: '0.25.1',
context: 'init',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
})!;
expect(advisory).toContain('ACTION FOR THE AGENT');
expect(advisory).toContain('Ask the user');
expect(advisory).toContain('Do NOT scaffold without asking');
expect(advisory).toContain('user owns this decision');
});
it('names the version + context (init vs upgrade)', () => {
const { workspace, skillsDir } = scratchWorkspace([]);
const initAdvisory = buildAdvisory({
version: '0.25.1',
context: 'init',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
})!;
const upgradeAdvisory = buildAdvisory({
version: '0.25.1',
context: 'upgrade',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
})!;
expect(initAdvisory).toContain('0.25.1');
expect(initAdvisory).toContain('installed');
expect(upgradeAdvisory).toContain('0.25.1');
expect(upgradeAdvisory).toContain('upgraded to');
});
it('describes each skill with a one-line value prop', () => {
const { workspace, skillsDir } = scratchWorkspace([]);
const advisory = buildAdvisory({
version: '0.25.1',
context: 'init',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
})!;
expect(advisory).toContain('FLAGSHIP');
expect(advisory).toContain('two-column');
expect(advisory).toContain('verbatim');
expect(advisory).toContain('Brain-augmented web research');
});
it('shows the exact install command per the missing-set', () => {
const { workspace, skillsDir } = scratchWorkspace([]);
const advisory = buildAdvisory({
version: '0.25.1',
context: 'init',
targetWorkspace: workspace,
targetSkillsDir: skillsDir,
})!;
expect(advisory).toContain('gbrain skillpack scaffold --all');
expect(advisory).toContain('ACTION FOR THE AGENT');
});
});
describe('buildAdvisory — no workspace detected', () => {
it('still renders an advisory with a workspace-detection note', () => {
const advisory = buildAdvisory({
version: '0.25.1',
context: 'init',
targetWorkspace: '/non/existent/workspace-xyz-' + Math.random().toString(36).slice(2),
targetSkillsDir: '/non/existent/skills-xyz-' + Math.random().toString(36).slice(2),
});
expect(advisory).not.toBeNull();
// No workspace -> empty installed set -> all recommended treated as missing.
expect(advisory).toContain('gbrain skillpack scaffold --all');
});
});