|
| 1 | +import assert from 'node:assert/strict'; |
| 2 | +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; |
| 3 | +import { tmpdir } from 'node:os'; |
| 4 | +import { dirname, join, resolve } from 'node:path'; |
| 5 | +import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 6 | +import test from 'node:test'; |
| 7 | + |
| 8 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 9 | +const repoRoot = resolve(__dirname, '../..'); |
| 10 | +const extensionSource = resolve(repoRoot, '.omp/extensions/superpowers.ts'); |
| 11 | +const roots = []; |
| 12 | + |
| 13 | +test.afterEach(() => { |
| 14 | + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); |
| 15 | +}); |
| 16 | + |
| 17 | +// Installs the real extension file into a throwaway package root, so each test |
| 18 | +// controls the bundled using-superpowers skill the extension reads. |
| 19 | +function install(skill) { |
| 20 | + const root = mkdtempSync(join(tmpdir(), 'superpowers-omp-')); |
| 21 | + roots.push(root); |
| 22 | + mkdirSync(join(root, '.omp', 'extensions'), { recursive: true }); |
| 23 | + copyFileSync(extensionSource, join(root, '.omp', 'extensions', 'superpowers.ts')); |
| 24 | + mkdirSync(join(root, 'skills', 'using-superpowers'), { recursive: true }); |
| 25 | + if (skill !== undefined) writeSkill(root, skill); |
| 26 | + return root; |
| 27 | +} |
| 28 | + |
| 29 | +function writeSkill(root, body) { |
| 30 | + writeFileSync(join(root, 'skills', 'using-superpowers', 'SKILL.md'), body); |
| 31 | +} |
| 32 | + |
| 33 | +async function load(root) { |
| 34 | + const handlers = new Map(); |
| 35 | + const omp = { |
| 36 | + on(event, handler) { |
| 37 | + assert.equal(handlers.has(event), false, `duplicate ${event} handler`); |
| 38 | + handlers.set(event, handler); |
| 39 | + }, |
| 40 | + }; |
| 41 | + const mod = await import(pathToFileURL(join(root, '.omp', 'extensions', 'superpowers.ts')).href); |
| 42 | + mod.default(omp); |
| 43 | + const ctx = { cwd: root, hasUI: false }; |
| 44 | + const request = [{ role: 'user', content: 'Let\'s make a react todo list', timestamp: 1 }]; |
| 45 | + return { |
| 46 | + handlers, |
| 47 | + request, |
| 48 | + async run(systemPrompt = ['Base system prompt.']) { |
| 49 | + return handlers.get('before_agent_start')({ type: 'before_agent_start', prompt: 'go', systemPrompt }, ctx); |
| 50 | + }, |
| 51 | + async context(messages = request) { |
| 52 | + const result = await handlers.get('context')({ type: 'context', messages }, ctx); |
| 53 | + return result?.messages ?? messages; |
| 54 | + }, |
| 55 | + async emit(type) { |
| 56 | + await handlers.get(type)?.({ type }, ctx); |
| 57 | + }, |
| 58 | + }; |
| 59 | +} |
| 60 | + |
| 61 | +const text = (message) => JSON.stringify(message.content); |
| 62 | + |
| 63 | +test('package.json declares the native OMP entry and keeps the Pi entry', () => { |
| 64 | + const pkg = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8')); |
| 65 | + assert.deepEqual(pkg.omp, { extensions: ['./.omp/extensions/superpowers.ts'] }); |
| 66 | + assert.deepEqual(pkg.pi.extensions, ['./.pi/extensions/superpowers.ts']); |
| 67 | +}); |
| 68 | + |
| 69 | +test('delivers the bootstrap as one leading user message, never in the system prompt', async () => { |
| 70 | + const omp = await load(install('\uFEFF---\r\nname: frontmatter-only\r\n---\r\n\r\n# Using Superpowers\r\nInvoke skills first.\r\n')); |
| 71 | + assert.equal(await omp.run(), undefined, 'system prompt and history stay untouched'); |
| 72 | + |
| 73 | + const messages = await omp.context(); |
| 74 | + assert.equal(messages.length, 2); |
| 75 | + assert.equal(messages[0].role, 'user'); |
| 76 | + assert.match(text(messages[0]), /You have superpowers/); |
| 77 | + assert.match(text(messages[0]), /Invoke skills first\./); |
| 78 | + assert.match(text(messages[0]), /OMP tool mapping/); |
| 79 | + assert.doesNotMatch(text(messages[0]), /frontmatter-only/); |
| 80 | + assert.equal(messages[1], omp.request[0]); |
| 81 | + assert.equal(omp.request.length, 1, 'the caller\'s array is not mutated'); |
| 82 | + assert.deepEqual(await omp.context(structuredClone(messages)), messages, 'a re-sent view is not duplicated'); |
| 83 | +}); |
| 84 | + |
| 85 | +// The shared Pi extension clears its flag at agent_end, so on OMP only the first |
| 86 | +// run of a session saw the bootstrap. Every provider request must carry it. |
| 87 | +test('keeps the bootstrap on later runs and after compaction', async () => { |
| 88 | + const omp = await load(install('Policy stays available.')); |
| 89 | + await omp.run(); |
| 90 | + await omp.context(); |
| 91 | + await omp.emit('agent_end'); |
| 92 | + // Compaction retries and queued follow-ups can reach the provider without a |
| 93 | + // new before_agent_start. |
| 94 | + assert.match(text((await omp.context())[0]), /Policy stays available\./); |
| 95 | + await omp.run(); |
| 96 | + assert.match(text((await omp.context())[0]), /Policy stays available\./); |
| 97 | + |
| 98 | + await omp.emit('session_compact'); |
| 99 | + const summary = { role: 'compactionSummary', summary: 'Earlier work', tokensBefore: 42, timestamp: 2 }; |
| 100 | + const compacted = await omp.context([summary, ...omp.request]); |
| 101 | + assert.equal(compacted.length, 3); |
| 102 | + assert.equal(compacted[0], summary, 'the bootstrap follows compaction summaries'); |
| 103 | + assert.match(text(compacted[1]), /Policy stays available\./); |
| 104 | + assert.equal(compacted[2], omp.request[0]); |
| 105 | +}); |
| 106 | + |
| 107 | +test('keeps the bootstrap after a branch summary', async () => { |
| 108 | + const omp = await load(install('Branch policy.')); |
| 109 | + await omp.run(); |
| 110 | + const summary = { role: 'branchSummary', summary: 'Abandoned branch', fromId: 'x', timestamp: 2 }; |
| 111 | + const messages = await omp.context([summary, ...omp.request]); |
| 112 | + assert.equal(messages[0], summary); |
| 113 | + assert.match(text(messages[1]), /Branch policy\./); |
| 114 | +}); |
| 115 | + |
| 116 | +test('refreshes an updated skill at the next run, not mid-run', async () => { |
| 117 | + const root = install('Original policy.'); |
| 118 | + const omp = await load(root); |
| 119 | + await omp.run(); |
| 120 | + writeSkill(root, 'Updated policy.'); |
| 121 | + assert.match(text((await omp.context())[0]), /Original policy\./); |
| 122 | + await omp.run(); |
| 123 | + const messages = await omp.context(); |
| 124 | + assert.equal(messages.length, 2); |
| 125 | + assert.match(text(messages[0]), /Updated policy\./); |
| 126 | +}); |
| 127 | + |
| 128 | +test('a missing skill warns visibly, injects nothing, and recovers after repair', async () => { |
| 129 | + const root = install('Installed policy.'); |
| 130 | + const omp = await load(root); |
| 131 | + await omp.run(); |
| 132 | + const previous = await omp.context(); |
| 133 | + rmSync(join(root, 'skills', 'using-superpowers', 'SKILL.md')); |
| 134 | + |
| 135 | + const missing = await omp.run(); |
| 136 | + assert.equal(missing.message.display, true); |
| 137 | + assert.match(missing.message.content, /SKILL\.md/); |
| 138 | + assert.equal(missing.systemPrompt, undefined); |
| 139 | + assert.deepEqual(await omp.context(previous), omp.request, 'stale policy is removed'); |
| 140 | + // The warning is persisted in the session, so a lasting failure reports once. |
| 141 | + assert.equal(await omp.run(), undefined); |
| 142 | + |
| 143 | + writeSkill(root, 'Repaired policy.'); |
| 144 | + assert.equal(await omp.run(), undefined); |
| 145 | + assert.match(text((await omp.context())[0]), /Repaired policy\./); |
| 146 | + rmSync(join(root, 'skills', 'using-superpowers', 'SKILL.md')); |
| 147 | + assert.match((await omp.run()).message.content, /SKILL\.md/, 'a new failure after recovery warns again'); |
| 148 | + await omp.emit('session_switch'); |
| 149 | + assert.match((await omp.run()).message.content, /SKILL\.md/, 'another session is warned too'); |
| 150 | +}); |
| 151 | + |
| 152 | +test('a skill without a body warns instead of injecting an empty bootstrap', async () => { |
| 153 | + const omp = await load(install('---\nname: using-superpowers\n---\n')); |
| 154 | + assert.match((await omp.run()).message.content, /no content/); |
| 155 | + assert.deepEqual(await omp.context(), omp.request); |
| 156 | +}); |
| 157 | + |
| 158 | +test('quoted bootstrap text neither suppresses the bootstrap nor removes user messages', async () => { |
| 159 | + const omp = await load(install('Real policy.')); |
| 160 | + const quoted = [{ role: 'user', content: 'You have superpowers. <EXTREMELY_IMPORTANT>', timestamp: 1 }]; |
| 161 | + await omp.run(); |
| 162 | + const messages = await omp.context(quoted); |
| 163 | + assert.equal(messages.length, 2); |
| 164 | + assert.match(text(messages[0]), /Real policy\./); |
| 165 | + assert.equal(messages[1], quoted[0]); |
| 166 | +}); |
| 167 | + |
| 168 | +test('session changes drop the old bootstrap and ignore a load that finishes late', async () => { |
| 169 | + const root = install('Old session policy.'); |
| 170 | + const omp = await load(root); |
| 171 | + await omp.run(); |
| 172 | + const old = await omp.context(); |
| 173 | + for (const event of ['session_switch', 'session_branch', 'session_tree', 'session_shutdown', 'session_start']) { |
| 174 | + await omp.run(); |
| 175 | + await omp.emit(event); |
| 176 | + assert.deepEqual(await omp.context(old), omp.request, `${event} clears the bootstrap`); |
| 177 | + } |
| 178 | + |
| 179 | + const pending = omp.run(); |
| 180 | + await omp.emit('session_switch'); |
| 181 | + await pending; |
| 182 | + assert.deepEqual(await omp.context(), omp.request, 'a load from the previous session is discarded'); |
| 183 | +}); |
0 commit comments