Files
gbrain/test/advisor-op-gate.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

50 lines
1.9 KiB
TypeScript

/**
* Tests for the `advisor` MCP op gate (T7 / C3): remote callers need
* mcp.publish_advisor; local callers bypass; the op is read-only (drops
* workspace-dependent findings over MCP via runAdvisor's remote filter).
*/
import { describe, test, expect } from 'bun:test';
import { operationsByName, OperationError, type OperationContext } from '../src/core/operations.ts';
const advisor = operationsByName['advisor']!;
function ctx(over: Partial<OperationContext>, cfg: Record<string, string | null> = {}): OperationContext {
return {
engine: {
getConfig: async (k: string) => cfg[k] ?? null,
getStats: async () => { throw new Error('no'); },
getHealth: async () => { throw new Error('no'); },
executeRaw: async () => { throw new Error('no'); },
} as unknown as OperationContext['engine'],
config: {} as OperationContext['config'],
logger: { info() {}, warn() {}, error() {}, debug() {} } as unknown as OperationContext['logger'],
dryRun: false,
remote: true,
...over,
} as OperationContext;
}
describe('advisor op gate', () => {
test('op exists, is read-scoped and not localOnly (exposed over MCP)', () => {
expect(advisor).toBeDefined();
expect(advisor.scope).toBe('read');
expect(advisor.localOnly).not.toBe(true);
});
test('remote without mcp.publish_advisor → permission_denied', async () => {
await expect(advisor.handler(ctx({ remote: true }), {})).rejects.toBeInstanceOf(OperationError);
});
test('remote WITH mcp.publish_advisor → returns a report', async () => {
const report = (await advisor.handler(ctx({ remote: true }, { 'mcp.publish_advisor': 'true' }), {})) as {
findings: unknown[];
};
expect(Array.isArray(report.findings)).toBe(true);
});
test('local caller bypasses the gate', async () => {
const report = (await advisor.handler(ctx({ remote: false }), {})) as { findings: unknown[] };
expect(Array.isArray(report.findings)).toBe(true);
});
});