generalize continuity plugin engine and generic adapter

This commit is contained in:
Eve
2026-04-24 19:28:20 +08:00
parent ed4fe3ea6c
commit 1fe6474009
13 changed files with 679 additions and 153 deletions

View File

@@ -52,6 +52,19 @@ test('normalizes missing fields from defaults', () => {
assert.equal(normalized.receiptDir, defaultConfig.receiptDir);
assert.deepEqual(normalized.legalTerminalStates, defaultConfig.legalTerminalStates);
assert.notEqual(normalized.legalTerminalStates, defaultConfig.legalTerminalStates);
assert.equal(normalized.adapter.genericPreflight.enabled, true);
});
test('normalizes generic preflight adapter block label', () => {
const normalized = normalizeContinuityConfig({
adapter: {
genericPreflight: {
injectBlockLabel: ' CUSTOM_GENERIC_GATE ',
},
},
});
assert.equal(normalized.adapter.genericPreflight.injectBlockLabel, 'CUSTOM_GENERIC_GATE');
});
test('rejects non-array legalTerminalStates', () => {
@@ -94,6 +107,17 @@ test('rejects malformed adapter.forceRecall shape', () => {
assert.match(result.errors.join('\n'), /adapter\.forceRecall/);
});
test('rejects malformed adapter.genericPreflight shape', () => {
const result = validateContinuityConfig({
adapter: {
genericPreflight: false,
},
});
assert.equal(result.ok, false);
assert.match(result.errors.join('\n'), /adapter\.genericPreflight/);
});
test('rejects malformed adapter.forceRecall.enabled type', () => {
const result = validateContinuityConfig({
adapter: {
@@ -120,6 +144,32 @@ test('rejects malformed adapter.forceRecall.injectBlockLabel type', () => {
assert.match(result.errors.join('\n'), /injectBlockLabel/);
});
test('rejects malformed adapter.genericPreflight.enabled type', () => {
const result = validateContinuityConfig({
adapter: {
genericPreflight: {
enabled: 'yes',
},
},
});
assert.equal(result.ok, false);
assert.match(result.errors.join('\n'), /adapter\.genericPreflight\.enabled/);
});
test('rejects malformed adapter.genericPreflight.injectBlockLabel type', () => {
const result = validateContinuityConfig({
adapter: {
genericPreflight: {
injectBlockLabel: 42,
},
},
});
assert.equal(result.ok, false);
assert.match(result.errors.join('\n'), /adapter\.genericPreflight\.injectBlockLabel/);
});
test('rejects unknown top-level key', () => {
const result = validateContinuityConfig({
unexpected: true,

View File

@@ -1,8 +1,10 @@
import assert from 'node:assert/strict';
import plugin, {
createForceRecallContinuityAdapter,
createGenericPreflightContinuityAdapter,
defaultConfig,
evaluateContinuity,
runManualContinuityPreflight,
} from '../src/index.mjs';
function test(name, fn) {
@@ -19,6 +21,8 @@ test('index exports plugin surface', () => {
assert.equal(plugin.name, '@openclaw/plugin-continuity');
assert.equal(typeof evaluateContinuity, 'function');
assert.equal(defaultConfig.adapter.forceRecall.enabled, true);
assert.equal(defaultConfig.adapter.genericPreflight.enabled, true);
assert.equal(typeof plugin.runGenericPreflightContinuityAdapter, 'function');
});
test('adapter preserves current hook parity for plain wrapper next-action mapping', () => {
@@ -36,6 +40,8 @@ test('adapter preserves current hook parity for plain wrapper next-action mappin
});
assert.equal(out.result.ok, true);
assert.equal(out.meta.adapterName, 'force-recall');
assert.equal(out.meta.hostAgnostic, true);
assert.match(out.block, /status=pass/);
});
@@ -60,4 +66,41 @@ test('adapter fails when planner-derived auto-next boundary exists without dispa
assert.match(out.block, /continuity_failure/);
});
test('generic preflight adapter evaluates host-agnostic source payload', () => {
const adapter = createGenericPreflightContinuityAdapter(defaultConfig);
const out = adapter.evaluate({
planId: 'plan-generic',
currentTask: 'task-generic',
taskState: 'complete',
nextTaskKnown: true,
sameApprovedPlan: true,
taskBoundaryStop: true,
nextTaskId: 'task-next',
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
replyClosureState: 'completed',
dispatchReceipt: null,
});
assert.equal(out.result.ok, false);
assert.equal(out.result.reason, 'missing_auto_next_dispatch');
assert.equal(out.meta.adapterName, 'generic-preflight');
assert.equal(out.meta.hostAgnostic, true);
assert.equal(out.input.planId, 'plan-generic');
});
test('manual continuity preflight runner works without force-recall hook', () => {
const out = runManualContinuityPreflight({
config: defaultConfig,
planId: 'plan-manual',
currentTask: 'task-manual',
taskState: 'complete',
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
replyClosureState: 'waiting_user',
});
assert.equal(out.result.ok, true);
assert.match(out.block, /APPROVED_PLAN_CONTINUITY_GATE/);
assert.equal(out.meta.adapterName, 'generic-preflight');
});
console.log('continuity.plugin.test.mjs PASS');

View File

@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import plugin, {
runForceRecallContinuityAdapter,
runGenericPreflightContinuityAdapter,
validateContinuityConfig,
} from '../src/index.mjs';
@@ -25,4 +26,26 @@ const smoke = runForceRecallContinuityAdapter({
assert.equal(smoke.result.ok, false);
assert.equal(smoke.result.reason, 'missing_auto_next_dispatch');
assert.match(smoke.block, /APPROVED_PLAN_CONTINUITY_GATE/);
assert.equal(smoke.meta.adapterName, 'force-recall');
const genericSmoke = runGenericPreflightContinuityAdapter({
config: plugin.defaultConfig,
source: {
planId: 'plan-generic-smoke',
currentTask: 'task-9',
taskState: 'complete',
nextTaskKnown: true,
sameApprovedPlan: true,
taskBoundaryStop: true,
nextTaskId: 'task-10',
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
replyClosureState: 'completed',
dispatchReceipt: null,
},
});
assert.equal(genericSmoke.result.ok, false);
assert.equal(genericSmoke.result.reason, 'missing_auto_next_dispatch');
assert.match(genericSmoke.block, /APPROVED_PLAN_CONTINUITY_GATE/);
assert.equal(genericSmoke.meta.adapterName, 'generic-preflight');
console.log('continuity.smoke.test.mjs PASS');