Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/product/generation/workforce-persona-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@ describe('workforce persona workflow writer', () => {
expect(parsed.metadata).toMatchObject({ workflowName: 'persona' });
});

it('accepts multiline run options when cwd is explicit', () => {
const parsed = parsePersonaWorkflowResponse(JSON.stringify({
artifact: {
path: 'workflows/generated/persona.ts',
content: multilineRunSource(),
},
metadata: {
workflowName: 'persona',
agents: ['lead'],
},
}), 'workflows/generated/persona.ts');

expect(parsed.responseFormat).toBe('structured-json');
expect(parsed.content).toContain('cwd: process.cwd()');
expect(parsed.metadata).toMatchObject({ workflowName: 'persona' });
});

it('still rejects persona artifacts that run without an explicit cwd', () => {
expect(() => parsePersonaWorkflowResponse(JSON.stringify({
artifact: {
path: 'workflows/generated/persona.ts',
content: workflowSource().replace('.run({ cwd: process.cwd() });', '.run();'),
},
metadata: {
workflowName: 'persona',
agents: ['lead'],
},
}), 'workflows/generated/persona.ts')).toThrow(/explicit cwd/);
});

it('recovers expected artifact content from disk when structured output omits inline content', () => {
const repoRoot = mkdtempSync(join(tmpdir(), 'ricky-persona-response-'));
const artifactPath = 'workflows/generated/persona.ts';
Expand Down Expand Up @@ -644,6 +674,18 @@ function workflowSource(): string {
].join('\n');
}

function multilineRunSource(): string {
return workflowSource().replace(
' .run({ cwd: process.cwd() });',
[
' .run({',
' cwd: process.cwd(),',
' timeoutMs: 120000,',
' });',
].join('\n'),
);
}

function spec(overrides: { description?: string; targetFiles?: string[] } = {}): NormalizedWorkflowSpec {
const description = overrides.description ?? 'Generate a workflow for deterministic product work.';
const rawPayload: RawSpecPayload = {
Expand Down
6 changes: 5 additions & 1 deletion src/product/generation/workforce-persona-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,11 +723,15 @@ function validateArtifactContent(content: string): void {
if (!/\bworkflow\(/.test(content)) {
throw new WorkforcePersonaWriterError('Workforce persona artifact does not call workflow().');
}
if (!/\.run\(\{ cwd: process\.cwd\(\) \}\)/.test(content)) {
if (!hasExplicitRunCwd(content)) {
throw new WorkforcePersonaWriterError('Workforce persona artifact must run with explicit cwd.');
}
}

function hasExplicitRunCwd(content: string): boolean {
return /\.run\s*\(\s*\{[\s\S]*?\bcwd\s*:\s*process\.cwd\s*\(\s*\)[\s\S]*?\}\s*\)/.test(content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Constrain cwd check to the same .run(...) call

The new regex can match across unrelated code, so a workflow that calls .run({ ... }) without cwd can still pass validation if any later object contains cwd: process.cwd() followed by a ) (for example in another helper call). This weakens the explicit-cwd guarantee and can allow invalid artifacts through pre-write validation, leading to runtime execution from the wrong directory. The check should ensure cwd is inside the same .run({ ... }) argument object rather than anywhere later in the file.

Useful? React with 👍 / 👎.

}

function validateMetadata(metadata: Record<string, unknown>): void {
if (Object.keys(metadata).length === 0) {
throw new WorkforcePersonaWriterError('Workforce persona response metadata block is required.');
Expand Down