|
|
@@ -0,0 +1,101 @@
|
|
|
+import assert from 'node:assert/strict';
|
|
|
+import test from 'node:test';
|
|
|
+import { PlanRefStore } from '../core/plan-ref-store.mjs';
|
|
|
+import { PlanTokenCodec } from '../core/plan-token.mjs';
|
|
|
+import { runGenerate } from './generate.mjs';
|
|
|
+import { runPlan } from './plan.mjs';
|
|
|
+
|
|
|
+test('plan reference hands the exact public plan to generate without paid execution before confirmation', async () => {
|
|
|
+ const planRefStore = new PlanRefStore();
|
|
|
+ const planned = await runPlan({ request: '规划七张测试套图' }, { planRefStore });
|
|
|
+ assert.equal(planned.status, 'ok');
|
|
|
+ assert.match(planned.data.planRef, /^pref_/);
|
|
|
+ const generated = await runGenerate({
|
|
|
+ planRef: planned.data.planRef,
|
|
|
+ confirmed: false,
|
|
|
+ sessionId: 'session',
|
|
|
+ promptId: 'prompt'
|
|
|
+ }, { planRefStore });
|
|
|
+ assert.equal(generated.status, 'needs_confirmation');
|
|
|
+ assert.equal(generated.data.plan.planHash, planned.data.plan.planHash);
|
|
|
+});
|
|
|
+
|
|
|
+test('restart-safe plan token restores the exact plan in a fresh process context', async () => {
|
|
|
+ const secret = 'test-only-plan-token-secret-32-bytes';
|
|
|
+ const workspaceRoot = process.cwd();
|
|
|
+ const planned = await runPlan({
|
|
|
+ request: '规划七张测试套图', sessionId: 'session-a', promptId: 'planning-prompt'
|
|
|
+ }, {
|
|
|
+ planRefStore: new PlanRefStore(),
|
|
|
+ planTokenCodec: new PlanTokenCodec({ secret }),
|
|
|
+ workspaceRoot
|
|
|
+ });
|
|
|
+ assert.equal(planned.status, 'ok');
|
|
|
+ assert.match(planned.data.planToken, /^ptk1\./);
|
|
|
+
|
|
|
+ const generated = await runGenerate({
|
|
|
+ planToken: planned.data.planToken,
|
|
|
+ confirmedPlanId: planned.data.plan.planId,
|
|
|
+ confirmed: false,
|
|
|
+ sessionId: 'session-a',
|
|
|
+ promptId: 'confirmation-prompt'
|
|
|
+ }, {
|
|
|
+ planRefStore: new PlanRefStore(),
|
|
|
+ planTokenCodec: new PlanTokenCodec({ secret }),
|
|
|
+ workspaceRoot
|
|
|
+ });
|
|
|
+ assert.equal(generated.status, 'needs_confirmation');
|
|
|
+ assert.equal(generated.data.plan.planHash, planned.data.plan.planHash);
|
|
|
+});
|
|
|
+
|
|
|
+test('plan token rejects cross-session, wrong-plan and ambiguous carriers before execution', async () => {
|
|
|
+ const secret = 'test-only-plan-token-secret-32-bytes';
|
|
|
+ const workspaceRoot = process.cwd();
|
|
|
+ const codec = new PlanTokenCodec({ secret });
|
|
|
+ const first = await runPlan({
|
|
|
+ request: '第一个七图计划', sessionId: 'session-a', promptId: 'prompt-a'
|
|
|
+ }, { planTokenCodec: codec, planRefStore: new PlanRefStore(), workspaceRoot });
|
|
|
+ const second = await runPlan({
|
|
|
+ request: '第二个详情计划', preset: 'detail-page', sessionId: 'session-a', promptId: 'prompt-b'
|
|
|
+ }, { planTokenCodec: codec, planRefStore: new PlanRefStore(), workspaceRoot });
|
|
|
+ assert.notEqual(first.data.plan.planId, second.data.plan.planId);
|
|
|
+
|
|
|
+ for (const input of [
|
|
|
+ { sessionId: 'session-b', confirmedPlanId: first.data.plan.planId },
|
|
|
+ { sessionId: 'session-a', confirmedPlanId: second.data.plan.planId }
|
|
|
+ ]) {
|
|
|
+ const rejected = await runGenerate({
|
|
|
+ planToken: first.data.planToken, confirmed: true, promptId: 'confirmation', ...input
|
|
|
+ }, { planTokenCodec: new PlanTokenCodec({ secret }), workspaceRoot });
|
|
|
+ assert.equal(rejected.status, 'needs_input');
|
|
|
+ }
|
|
|
+ const missingPlan = await runGenerate({
|
|
|
+ planToken: first.data.planToken, confirmed: true, sessionId: 'session-a', promptId: 'confirmation'
|
|
|
+ }, { planTokenCodec: new PlanTokenCodec({ secret }), workspaceRoot });
|
|
|
+ assert.equal(missingPlan.status, 'needs_input');
|
|
|
+ const ambiguous = await runGenerate({
|
|
|
+ planToken: first.data.planToken, planRef: first.data.planRef,
|
|
|
+ confirmedPlanId: first.data.plan.planId, confirmed: true, sessionId: 'session-a', promptId: 'confirmation'
|
|
|
+ }, { planTokenCodec: new PlanTokenCodec({ secret }), workspaceRoot });
|
|
|
+ assert.equal(ambiguous.status, 'needs_input');
|
|
|
+});
|
|
|
+
|
|
|
+test('generate keeps full-plan compatibility and rejects ambiguous or expired references', async () => {
|
|
|
+ let clock = 10;
|
|
|
+ const planRefStore = new PlanRefStore({ ttlMs: 5, now: () => clock });
|
|
|
+ const planned = await runPlan({ request: '规划七张测试套图' }, { planRefStore });
|
|
|
+ const legacy = await runGenerate({
|
|
|
+ plan: planned.data.plan, confirmed: false, sessionId: 'session', promptId: 'legacy'
|
|
|
+ }, { planRefStore });
|
|
|
+ assert.equal(legacy.status, 'needs_confirmation');
|
|
|
+ const ambiguous = await runGenerate({
|
|
|
+ plan: planned.data.plan, planRef: planned.data.planRef, confirmed: false, sessionId: 'session', promptId: 'ambiguous'
|
|
|
+ }, { planRefStore });
|
|
|
+ assert.equal(ambiguous.status, 'needs_input');
|
|
|
+ clock = 15;
|
|
|
+ const expired = await runGenerate({
|
|
|
+ planRef: planned.data.planRef, confirmed: true, sessionId: 'session', promptId: 'expired'
|
|
|
+ }, { planRefStore });
|
|
|
+ assert.equal(expired.status, 'needs_input');
|
|
|
+ assert.match(expired.assistantMessage, /本地规划/);
|
|
|
+});
|