diff --git a/scripts/test_force_recall_long_task_preflight.mjs b/scripts/test_force_recall_long_task_preflight.mjs new file mode 100644 index 0000000..3b64fec --- /dev/null +++ b/scripts/test_force_recall_long_task_preflight.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { stripTypeScriptTypes } from 'node:module'; + +const __dirname = path.dirname(new URL(import.meta.url).pathname); +const repoRoot = path.resolve(__dirname, '..'); +const handlerPath = path.join(repoRoot, 'hooks', 'force-recall', 'handler.ts'); +const wrapperPath = path.join(repoRoot, 'scripts', 'long_task_governor_wrapper.mjs'); + +async function importTsModule(tsPath) { + const source = await fs.readFile(tsPath, 'utf8'); + const jsSource = stripTypeScriptTypes(source, { mode: 'strip' }); + const dataUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(jsSource)}\n//# sourceURL=${encodeURIComponent(pathToFileURL(tsPath).href)}`; + return import(dataUrl); +} + +async function main() { + await fs.access(wrapperPath); + const { default: forceRecall } = await importTsModule(handlerPath); + assert.equal(typeof forceRecall, 'function', 'force-recall handler should export default function'); + + const requestText = [ + 'Please inspect the workspace files and verify the hook injection path.', + 'I need you to review the behavior, choose the final accept/reject decision,', + 'and continue in background with a follow-up later.', + ].join(' '); + + const event = { + type: 'message', + action: 'preprocessed', + context: { + workspaceDir: repoRoot, + body: requestText, + bodyForAgent: requestText, + }, + }; + + await forceRecall(event); + + const injected = event.context?.bodyForAgent; + assert.equal(typeof injected, 'string', 'event.context.bodyForAgent should be a string after handler runs'); + + const expectedSnippets = [ + '[LONG_TASK_GOVERNOR_PREFLIGHT]', + 'classification=long_task', + 'silentLaunchOk=false', + 'handoff.mode=button_path', + 'HARD_GATE:', + ]; + + for (const snippet of expectedSnippets) { + assert.match(injected, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `missing snippet: ${snippet}`); + } + + const summary = { + ok: true, + checked: expectedSnippets, + bodyPreview: injected.split('\n').slice(0, 20), + }; + + process.stdout.write(JSON.stringify(summary, null, 2) + '\n'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +});