feat: implement auto-scaffolding for project workflow files in bare repos
ci / check (macos-latest) (push) Canceled after 0s
ci / check (ubuntu-latest) (push) Canceled after 0s
ci / check (windows-latest) (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-09-11 18:52:50 +07:00
parent 44bb4bebf1
commit 7d77408a41
9 changed files with 441 additions and 9 deletions
+73 -2
View File
@@ -2,7 +2,7 @@ import { usageOf } from './helpers';
import { expect, test } from 'bun:test';
import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
import { mkdtempSync, rmSync } from 'node:fs';
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Session } from '../src/session';
@@ -85,7 +85,14 @@ test('workflow disabled renders no policy even with a TODO.md', () =>
test('bare repo renders no workflow policy', () =>
inGitRepo(async () => {
const session = new Session({ model: new MockLanguageModelV4({ doStream: async () => stream([]) }), askApproval: async () => "deny" });
const session = new Session({
model: new MockLanguageModelV4({ doStream: async () => stream([]) }),
askApproval: async () => "deny",
// Without this, the first send() would auto-scaffold TODO.md/etc into
// the empty repo — that is the point of the feature, but this test is
// specifically about the policy wire-up, so keep the repo bare.
workflow: { autoScaffold: false },
});
const status = session.workflowStatus();
expect(status.hasTodo).toBe(false);
expect(status.hasRoadmap).toBe(false);
@@ -277,4 +284,68 @@ test('todo_write in a turn suppresses that turn\'s nudge', async () => {
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('first turn auto-scaffolds TODO.md/ROADMAP.md/docs/AGENTS.md in a bare repo', async () => {
const dir = mkdtempSync(join(tmpdir(), 'shiro-wf-autoscaffold'));
const orig = process.cwd();
process.chdir(dir);
try {
await Bun.write(join(dir, '.git', 'HEAD'), 'ref: refs/heads/main\n');
// The model returns the four workflow files on the scaffold call, then a
// plain text answer for the actual turn.
let call = 0;
const session = new Session({
model: new MockLanguageModelV4({
doStream: async () => {
call++;
if (call === 1) {
// Scaffold call: emit text-delta chunks carrying all four file blocks.
const body = [
'===FILE TODO.md===',
'# TODO\n\n## Now\n- first',
'===FILE ROADMAP.md===',
'# Roadmap\n\n## Next\n- plan',
'===FILE docs/README.md===',
'# Docs\n\nDeveloper docs.',
'===FILE AGENTS.md===',
'# AGENTS\n\nA test project.',
].join('\n');
return {
stream: simulateReadableStream({
chunks: [
{ type: 'text-start', id: 's' },
{ type: 'text-delta', id: 's', delta: body },
{ type: 'text-end', id: 's' },
{ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage },
],
chunkDelayInMs: null,
initialDelayInMs: null,
}),
};
}
// The actual turn: no tool calls, just text.
return stream(text('ok'));
},
}),
askApproval: async () => 'once',
});
const notices: string[] = [];
for await (const ev of session.send('hello')) {
if (ev.type === 'notice') notices.push(ev.text);
}
// The scaffold notice fired before the real turn.
expect(notices.some((n) => n.startsWith('scaffolded project workflow files'))).toBe(true);
// All four files now exist at the git root.
expect(existsSync(join(dir, 'TODO.md'))).toBe(true);
expect(existsSync(join(dir, 'ROADMAP.md'))).toBe(true);
expect(existsSync(join(dir, 'docs', 'README.md'))).toBe(true);
expect(existsSync(join(dir, 'AGENTS.md'))).toBe(true);
} finally {
process.chdir(orig);
rmSync(dir, { recursive: true, force: true });
}
});