test(skill): add bb mail lifecycle and preflight coverage
This commit is contained in:
parent
a9566059ba
commit
003aba3179
58 changed files with 402 additions and 2142 deletions
124
skills/beadboard-driver/scripts/ensure-bb-mail-configured.mjs
Normal file
124
skills/beadboard-driver/scripts/ensure-bb-mail-configured.mjs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { findCommandInPath } from './lib/driver-lib.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function readMailDelegate(bdPath) {
|
||||
const result = spawnSync(bdPath, ['config', 'get', 'mail.delegate'], {
|
||||
stdio: 'pipe',
|
||||
shell: false,
|
||||
});
|
||||
|
||||
const stdout = result.stdout?.toString().trim() || '';
|
||||
const stderr = result.stderr?.toString().trim() || '';
|
||||
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
delegate: stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const bdPath = await findCommandInPath('bd');
|
||||
const shimPath = join(__dirname, 'bb-mail-shim.mjs');
|
||||
const expected = `node ${shimPath}`;
|
||||
|
||||
if (!bdPath) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'BD_NOT_FOUND',
|
||||
reason: 'Could not find bd in PATH.',
|
||||
remediation:
|
||||
process.platform === 'win32'
|
||||
? 'Primary: npm i -g beadboard. Fallback: powershell -ExecutionPolicy Bypass -File .\\install\\install.ps1. Then ensure bd is available in PATH.'
|
||||
: 'Primary: npm i -g beadboard. Fallback: bash ./install/install.sh. Then ensure bd is available in PATH.',
|
||||
expected_delegate: expected,
|
||||
delegate: null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const delegate = readMailDelegate(bdPath);
|
||||
|
||||
if (!delegate.ok || !delegate.delegate) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'MAIL_DELEGATE_MISSING',
|
||||
reason: delegate.stderr || 'mail.delegate is not configured.',
|
||||
remediation: `Run: bd config set mail.delegate "${expected}"`,
|
||||
expected_delegate: expected,
|
||||
delegate: delegate.delegate || null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (delegate.delegate !== expected) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'MAIL_DELEGATE_MISMATCH',
|
||||
reason: 'mail.delegate is set, but not to the BeadBoard bb-mail shim command.',
|
||||
remediation: `Run: bd config set mail.delegate "${expected}"`,
|
||||
expected_delegate: expected,
|
||||
delegate: delegate.delegate,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = (process.env.BB_AGENT || process.env.BD_ACTOR || '').trim();
|
||||
if (!actor) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'BB_AGENT_NOT_SET',
|
||||
reason: 'mail.delegate is configured, but BB_AGENT/BD_ACTOR is missing.',
|
||||
remediation: 'Set BB_AGENT (preferred) or BD_ACTOR before using bd mail.',
|
||||
expected_delegate: expected,
|
||||
delegate: delegate.delegate,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
expected_delegate: expected,
|
||||
delegate: delegate.delegate,
|
||||
actor,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
void main();
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve('.');
|
||||
const shimPath = path.resolve('skills/beadboard-driver/scripts/bb-mail-shim.mjs');
|
||||
const cliPath = path.resolve('src/cli/beadboard-cli.ts');
|
||||
const tsxLoader = path.resolve('node_modules/tsx/dist/loader.mjs');
|
||||
|
||||
async function withTempDir(run) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-skill-mail-it-'));
|
||||
try {
|
||||
await run(root);
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function randomAgent(base) {
|
||||
return `${base}-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`.toLowerCase();
|
||||
}
|
||||
|
||||
async function writeBbProxy(binDir) {
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
|
||||
const bbPath = path.join(binDir, 'bb');
|
||||
await fs.writeFile(
|
||||
bbPath,
|
||||
`#!/usr/bin/env sh\nexec node --import "${tsxLoader}" "${cliPath}" "$@"\n`,
|
||||
'utf8',
|
||||
);
|
||||
await fs.chmod(bbPath, 0o755);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const bbCmdPath = path.join(binDir, 'bb.cmd');
|
||||
await fs.writeFile(
|
||||
bbCmdPath,
|
||||
`@echo off\r\nnode --import "${tsxLoader}" "${cliPath}" %*\r\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runBb(args, env) {
|
||||
const bbExecutable = process.platform === 'win32' ? 'bb.cmd' : 'bb';
|
||||
const { stdout } = await execFileAsync(bbExecutable, args, {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
test('bb-mail integration contract: send -> inbox -> read -> ack lifecycle', async () => {
|
||||
await withTempDir(async (root) => {
|
||||
const binDir = path.join(root, 'bin');
|
||||
const homeDir = path.join(root, 'home');
|
||||
await writeBbProxy(binDir);
|
||||
|
||||
const sender = randomAgent('maf8-sender');
|
||||
const recipient = randomAgent('maf8-recipient');
|
||||
|
||||
const baseEnv = {
|
||||
...process.env,
|
||||
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
|
||||
HOME: homeDir,
|
||||
USERPROFILE: homeDir,
|
||||
};
|
||||
|
||||
const senderRegRaw = await runBb(['agent', 'register', '--name', sender, '--role', 'ui', '--json'], baseEnv);
|
||||
const senderReg = JSON.parse(senderRegRaw);
|
||||
assert.equal(senderReg.ok, true);
|
||||
|
||||
const recipientRegRaw = await runBb(['agent', 'register', '--name', recipient, '--role', 'graph', '--json'], baseEnv);
|
||||
const recipientReg = JSON.parse(recipientRegRaw);
|
||||
assert.equal(recipientReg.ok, true);
|
||||
|
||||
await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
shimPath,
|
||||
'send',
|
||||
'--to',
|
||||
recipient,
|
||||
'--bead',
|
||||
'beadboard-maf.8',
|
||||
'--category',
|
||||
'HANDOFF',
|
||||
'--subject',
|
||||
'Contract handoff',
|
||||
'--body',
|
||||
'Please validate and ack.',
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: { ...baseEnv, BB_AGENT: sender },
|
||||
},
|
||||
);
|
||||
|
||||
const inboxResult = await execFileAsync(process.execPath, [shimPath, 'inbox', '--state', 'unread', '--limit', '10'], {
|
||||
cwd: repoRoot,
|
||||
env: { ...baseEnv, BB_AGENT: recipient },
|
||||
});
|
||||
|
||||
const messageMatch = inboxResult.stdout.match(/\[([^\]]+)\]/);
|
||||
assert.ok(messageMatch, `expected message id in inbox output, got: ${inboxResult.stdout}`);
|
||||
const messageId = messageMatch[1];
|
||||
|
||||
await execFileAsync(process.execPath, [shimPath, 'read', messageId], {
|
||||
cwd: repoRoot,
|
||||
env: { ...baseEnv, BB_AGENT: recipient },
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [shimPath, 'ack', messageId], {
|
||||
cwd: repoRoot,
|
||||
env: { ...baseEnv, BB_AGENT: recipient },
|
||||
});
|
||||
|
||||
const ackedRaw = await runBb(['agent', 'inbox', '--agent', recipient, '--state', 'acked', '--limit', '25', '--json'], baseEnv);
|
||||
const acked = JSON.parse(ackedRaw);
|
||||
|
||||
assert.equal(acked.ok, true);
|
||||
assert.ok(Array.isArray(acked.data));
|
||||
assert.ok(acked.data.some((message) => message.message_id === messageId && message.state === 'acked'));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const scriptPath = path.resolve('skills/beadboard-driver/scripts/ensure-bb-mail-configured.mjs');
|
||||
|
||||
async function withTempDir(run) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-skill-mailcfg-'));
|
||||
try {
|
||||
await run(root);
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('ensure-bb-mail-configured contract: missing delegate shows remediation', async () => {
|
||||
await withTempDir(async (root) => {
|
||||
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
BB_AGENT: 'contract-agent',
|
||||
},
|
||||
});
|
||||
|
||||
const result = JSON.parse(stdout);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error_code, 'MAIL_DELEGATE_MISSING');
|
||||
assert.match(String(result.remediation), /bd config set mail\.delegate/i);
|
||||
});
|
||||
});
|
||||
|
|
@ -12,12 +12,25 @@ const __filename = fileURLToPath(import.meta.url);
|
|||
const __dirname = path.dirname(__filename);
|
||||
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'resolve-bb.mjs');
|
||||
|
||||
async function createRepoEntrypoint(repo) {
|
||||
await fs.mkdir(path.join(repo, 'tools'), { recursive: true });
|
||||
if (process.platform === 'win32') {
|
||||
const bbPath = path.join(repo, 'bb.ps1');
|
||||
await fs.writeFile(bbPath, 'echo ok', 'utf8');
|
||||
return bbPath;
|
||||
}
|
||||
const bbPath = path.join(repo, 'bin', 'beadboard.js');
|
||||
await fs.mkdir(path.dirname(bbPath), { recursive: true });
|
||||
await fs.writeFile(bbPath, '#!/usr/bin/env node\nconsole.log("ok");\n', 'utf8');
|
||||
await fs.chmod(bbPath, 0o755);
|
||||
return bbPath;
|
||||
}
|
||||
|
||||
test('resolve-bb contract: BB_REPO source', async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-skill-contract-resolve-'));
|
||||
try {
|
||||
const repo = path.join(root, 'beadboard');
|
||||
await fs.mkdir(path.join(repo, 'tools'), { recursive: true });
|
||||
await fs.writeFile(path.join(repo, 'bb.ps1'), 'echo ok', 'utf8');
|
||||
await createRepoEntrypoint(repo);
|
||||
|
||||
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||
env: { ...process.env, BB_REPO: repo, BB_SKILL_HOME: path.join(root, 'home'), PATH: '' },
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const tests = [
|
|||
path.join(__dirname, 'resolve-bb.contract.test.mjs'),
|
||||
path.join(__dirname, 'generate-agent-name.contract.test.mjs'),
|
||||
path.join(__dirname, 'session-preflight.contract.test.mjs'),
|
||||
path.join(__dirname, 'ensure-bb-mail-configured.contract.test.mjs'),
|
||||
path.join(__dirname, 'bb-mail-integration.contract.test.mjs'),
|
||||
path.join(__dirname, 'readiness-report.contract.test.mjs'),
|
||||
path.join(__dirname, 'diagnose-env.contract.test.mjs'),
|
||||
path.join(__dirname, 'heal-common-issues.contract.test.mjs'),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,20 @@ const __filename = fileURLToPath(import.meta.url);
|
|||
const __dirname = path.dirname(__filename);
|
||||
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'session-preflight.mjs');
|
||||
|
||||
async function createRepoEntrypoint(repo) {
|
||||
await fs.mkdir(path.join(repo, 'tools'), { recursive: true });
|
||||
if (process.platform === 'win32') {
|
||||
const bbPath = path.join(repo, 'bb.ps1');
|
||||
await fs.writeFile(bbPath, 'echo ok', 'utf8');
|
||||
return bbPath;
|
||||
}
|
||||
const bbPath = path.join(repo, 'bin', 'beadboard.js');
|
||||
await fs.mkdir(path.dirname(bbPath), { recursive: true });
|
||||
await fs.writeFile(bbPath, '#!/usr/bin/env node\nconsole.log("ok");\n', 'utf8');
|
||||
await fs.chmod(bbPath, 0o755);
|
||||
return bbPath;
|
||||
}
|
||||
|
||||
test('session-preflight contract: surfaces BD_NOT_FOUND when missing', async () => {
|
||||
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||
env: { ...process.env, PATH: '' },
|
||||
|
|
@ -28,9 +42,8 @@ test('session-preflight contract: succeeds with bd + BB_REPO', async () => {
|
|||
const toolsDir = path.join(root, 'tools');
|
||||
const bdExecutable = process.platform === 'win32' ? 'bd.cmd' : 'bd';
|
||||
const bdPath = path.join(toolsDir, bdExecutable);
|
||||
await fs.mkdir(path.join(repo, 'tools'), { recursive: true });
|
||||
await createRepoEntrypoint(repo);
|
||||
await fs.mkdir(toolsDir, { recursive: true });
|
||||
await fs.writeFile(path.join(repo, 'bb.ps1'), 'echo ok', 'utf8');
|
||||
if (process.platform === 'win32') {
|
||||
await fs.writeFile(bdPath, '@echo off\r\necho beads\r\n', 'utf8');
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue