Files
open-design/packages/plugin-runtime/tests/od-next-strategy-package.test.ts
424f5c68cc fix(od-next): remove output-contract gates and unblock continuation (#8356)
* fix(od-next): continue plans with keyed markers and honor explicit deliverables

* fix(od-next): retire output contracts and hand off continued tasks

* fix(od-next): record round settlement and recover production cold

* fix(od-next): resume marker sessions with context deltas

* fix(od-next): separate completion facts and honor plan-only intent

* fix(od-next): preserve physical failures and legacy chat compatibility

* test(web): align run recovery fixtures and await campaign mount

* fix(od-next): tolerate terminal production marker formatting

* fix(od-next): unblock follow-ups after a stop or unreadable history (#8423)

Treat a previous task this build cannot read as absent so the follow-up starts a new task, and stop counting a Run with a pending cancel as holding its conversation. Add end-to-end coverage for the marker continuation flow and restore the remote Workspace directory gate cases.

* docs: clarify focused local validation

Keep local daemon and web validation scoped to affected files and integration boundaries. Reserve full suites for PR CI unless the user explicitly requests them.

* fix(web): scope folded task completion to its final run

Keep the final physical Run key and conclusion together when OD Next history is folded or accumulated live. Reuse the existing predecessor offsets while retaining the full display history and remaining Todo snapshot.

Add regression coverage for completed and unfinished production, live continuation, multiple Runs, legacy history, wrong and quoted markers, question handoff, and failed or canceled Runs.

Validated the parent regression before the fix, 165 focused Web tests, workspace typecheck, and guard.

* fix(od-next): compact historical context without restricting clarification

Reuse confirmed artifact summaries for old assistant history, preserve cross-agent plans and original requests, and represent earlier questionnaires as neutral historical information without inferring answers.

Keep user replies, skips, changed answers, unpersisted bodies, examples, and unfamiliar content intact. Do not add question repetition checks, output gates, repair rounds, or limits on clarification.

Validated red regressions, shared context and Web provider tests, focused handoff/resume/cold-production coverage, real server and fake CLI input, workspace typecheck, and guard. The repeated-question server fixture remains successful.

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Cheems <hihicheems@gmail.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
2026-10-13 10:32:45 +00:00

168 lines
7.6 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { BundledStrategyDeclarationV2Schema } from '@open-design/contracts';
import { parseManifest } from '../src/index.js';
const pluginRoot = fileURLToPath(
new URL('../../../plugins/_official/scenarios/od-next-strategy/', import.meta.url),
);
const manifestSource = readFileSync(`${pluginRoot}/open-design.json`, 'utf8');
const parsed = parseManifest(manifestSource);
if (!parsed.ok) throw new Error(parsed.errors.join('\n'));
const manifest = parsed.manifest;
const declaration = BundledStrategyDeclarationV2Schema.parse(
(manifest.od as Record<string, unknown>)['strategy'],
);
const forbiddenContent = [
/acceptanceChecklist/i,
/evidence[ -]plan/i,
/quality[ -]score/i,
/judge(?:[ -]agent)?/i,
/artifact[ -]repair/i,
/candidate[ -]evidence[ -]bundle/i,
/completion[ -]gate/i,
/final[ -]evidence[ -]bundle/i,
/repair[ -]required/i,
/\brepeat\b/i,
/\bcritique\b/i,
/revalidation/i,
/critique(?:-theater)?/i,
/post[- ]build[\s\S]{0,80}(?:verify|inspect|check|review)/i,
/(?:screenshot|browser|dom)[\s\S]{0,80}(?:verify|inspect|check|review)/i,
];
describe('bundled OD Next Strategy V2 package', () => {
it('declares the inactive versioned asset set and exact planning recipe identity', () => {
expect(manifest).toMatchObject({
name: 'od-next-strategy',
version: '2.0.6',
od: {
kind: 'scenario',
hidden: true,
strategy: {
schema: 'open-design.bundled-strategy/v2',
id: 'od-next-strategy',
promptRecipe: 'od-next-plan-build-v2',
},
},
});
expect(declaration.assets.taskProfiles.map((profile) => [
profile.taskType,
profile.rollout,
profile.projectKinds,
])).toEqual([
['prototype', 'active', ['prototype']],
['ppt', 'active', ['deck']],
['marketing', 'active', ['image']],
['hyperframes', 'active', ['video']],
]);
});
it('declares discovery, plan, and generate without a repeating stage', () => {
expect(manifest.od?.pipeline?.stages).toEqual([
{ id: 'discovery', atoms: ['discovery-question-form'] },
{ id: 'plan', atoms: ['direction-picker', 'todo-write'] },
{ id: 'generate', atoms: ['file-write', 'live-artifact'] },
]);
expect(manifest.od?.pipeline?.stages.some((stage) => stage.repeat)).toBe(false);
});
it('ships every declared asset and keeps strategy content on the pre-Build side', () => {
const assetPaths = [
declaration.assets.core.path,
declaration.assets.orchestration.path,
...declaration.assets.taskProfiles.map((profile) => profile.path),
...declaration.assets.taskProfiles.flatMap((profile) => (profile.resources ?? []).map((resource) => resource.path)),
declaration.assets.taskProfileMapping.path,
];
expect(new Set(assetPaths).size).toBe(assetPaths.length);
for (const assetPath of assetPaths) {
const content = readFileSync(`${pluginRoot}/${assetPath.slice(2)}`, 'utf8');
expect(content.length, assetPath).toBeGreaterThan(100);
// Resources (shells, stylesheets) are quoted as facts, never as
// instructions, so the pre-Build vocabulary rule applies to prose only.
if (!assetPath.endsWith('.md')) continue;
for (const forbidden of forbiddenContent) {
expect(content, `${assetPath} must not match ${forbidden}`).not.toMatch(forbidden);
}
}
});
it('ships the three handheld shells and the layout primitives as prototype resources', () => {
const prototype = declaration.assets.taskProfiles.find((profile) => profile.taskType === 'prototype');
expect(prototype?.resources?.map((resource) => resource.path)).toEqual([
'./assets/task-profiles/prototype/device-frames/iphone.html',
'./assets/task-profiles/prototype/device-frames/android.html',
'./assets/task-profiles/prototype/device-frames/neutral.html',
'./assets/task-profiles/prototype/layout.css',
]);
const primitives = readFileSync(`${pluginRoot}/assets/task-profiles/prototype/layout.css`, 'utf8');
expect(primitives).toContain('/* OD-LAYOUT-PRIMITIVES v1');
expect(primitives).toContain('/* /OD-LAYOUT-PRIMITIVES v1 */');
expect(primitives).toMatch(/^@layer od-layout \{/m);
// Structure only: no palette, type, radius, or shadow opinions.
expect(primitives).not.toMatch(/(^|[^-])color\s*:|font-family|font-size|border-radius|box-shadow|background\s*:/);
for (const resource of (prototype?.resources ?? []).filter((r) => r.path.includes('/device-frames/'))) {
const shell = readFileSync(`${pluginRoot}/${resource.path.slice(2)}`, 'utf8');
expect(shell).toContain('data-phone-shell');
expect(shell).toContain('class="phone-content"');
expect(shell).toContain('APP CONTENT START');
expect(shell).toContain('APP CONTENT END');
expect(shell).toContain('--phone-safe-top');
expect(shell).toContain('@media (max-width: 480px)');
}
const ruleCard = readFileSync(`${pluginRoot}/${prototype!.path.slice(2)}`, 'utf8');
expect(ruleCard).toContain('### Handheld device shell');
for (const shell of ['.od-frames/iphone.html', '.od-frames/android.html', '.od-frames/neutral.html']) {
expect(ruleCard).toContain(shell);
}
expect(ruleCard).toContain('### Variable-length text and stacked information');
expect(ruleCard).toContain('.od-frames/layout.css');
expect(ruleCard).toContain('OD-LAYOUT-PRIMITIVES v1');
});
it('bounds functional SVG icons without sizing content SVGs', () => {
const prototype = declaration.assets.taskProfiles.find((profile) => profile.taskType === 'prototype');
const ruleCard = readFileSync(`${pluginRoot}/assets/task-profiles/prototype.md`, 'utf8');
const primitives = readFileSync(`${pluginRoot}/assets/task-profiles/prototype/layout.css`, 'utf8');
expect(prototype?.version).toBe('2.3.2');
expect(prototype?.resources?.find((resource) => resource.path.endsWith('/layout.css'))?.version)
.toBe('1.1.0');
expect(ruleCard).toContain('### Bound functional icon geometry');
expect(ruleCard).toContain('`.od-icon`');
expect(ruleCard).toContain('`--od-icon-size`');
expect(ruleCard).toMatch(/`viewBox`[^.\n]*(?:does not|is not)[^.\n]*(?:size|geometry)/i);
expect(ruleCard).toMatch(/parent selectors?/i);
expect(ruleCard).toMatch(/content SVG/i);
const iconRule = primitives.match(/\.od-icon\s*\{([^}]*)\}/s)?.[1];
expect(iconRule).toBeDefined();
expect(iconRule).toMatch(/display:\s*inline-block/);
expect(iconRule).toMatch(/inline-size:\s*var\(--od-icon-size,\s*1em\)/);
expect(iconRule).toMatch(/block-size:\s*var\(--od-icon-size,\s*1em\)/);
expect(iconRule).toMatch(/flex:\s*none/);
// Sizing by parent context recreated the original blind spot: each new
// context can miss a selector. The staged primitive stays opt-in so charts,
// plans, diagrams, logos, and illustrations keep their own geometry.
const uncommented = primitives.replace(/\/\*[\s\S]*?\*\//g, '');
const selectors = Array.from(uncommented.matchAll(/([^{}]+)\{/g), (match) => match[1] ?? '');
expect(selectors.some((selector) => /(^|[^.#[:\w-])svg(?=$|[^\w-])/i.test(selector))).toBe(false);
});
it('maps unknown project kinds to generic or blocked instead of guessing', () => {
const mapping = readFileSync(
`${pluginRoot}/${declaration.assets.taskProfileMapping.path.slice(2)}`,
'utf8',
);
expect(mapping).toContain('use task type `generic`');
expect(mapping).toContain('report blocked');
expect(mapping).toContain('Never select the nearest specialist profile by guesswork');
});
});