/** * prompt 子指令:agent 讀到轉接檔之後,靠它拿到流程正本的原文。 * * 這是全專案唯一輸出非 JSON 的路徑,所以這裡釘的第一件事就是「逐字」—— * 多一個換行、少一個空白,都會讓下游模型讀到的東西與正本不同。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { manifest, repoRoot, runBin } from './helpers/run-script.js'; import { fakePrompt, makeFakePlugin } from './helpers/fake-plugin.js'; const version = () => manifest().version; // ── 成功路徑:原樣輸出 ───────────────────────────────────────────── test('把流程正本逐字印出來,不包 JSON、不補字元', async (t) => { const text = fakePrompt('sdlc-plan'); const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': text } }); const { code, raw } = await plugin.run(['prompt', '--name', 'sdlc-plan']); assert.equal(code, 0); assert.equal(raw, text); }); test('輸出允許多行,且不是 JSON', async (t) => { const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': fakePrompt('sdlc-plan') } }); const { raw } = await plugin.run(['prompt', '--name', 'sdlc-plan']); assert.ok(raw.split('\n').length > 3); assert.throws(() => JSON.parse(raw.trim())); }); test('真正的流程正本也走得通,輸出逐字等於檔案內容', async () => { const path = join(repoRoot, 'prompts', 'sdlc-plan.md'); const { code, raw } = await runBin(['prompt', '--name', 'sdlc-plan']); assert.equal(code, 0); assert.equal(raw, readFileSync(path, 'utf8')); }); // ── 失敗路徑:仍走單行 JSON ──────────────────────────────────────── test('指令不存在時回可區分的錯誤碼,而不是印出空內容當成成功', async (t) => { const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': fakePrompt('sdlc-plan') } }); const { code, json } = await plugin.run(['prompt', '--name', 'sdlc-nope']); assert.equal(code, 1); assert.equal(json.ok, false); assert.equal(json.error.code, 'PROMPT_NOT_FOUND'); assert.match(json.error.message, /sdlc-nope/); }); test('未帶 --name 時沿用既有的 MISSING_FLAG', async (t) => { const plugin = makeFakePlugin(t); const { code, json } = await plugin.run(['prompt']); assert.equal(code, 1); assert.equal(json.error.code, 'MISSING_FLAG'); assert.match(json.error.message, /--name/); }); test('--name 不能拿來跳出 prompts 目錄', async (t) => { const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': fakePrompt('sdlc-plan') } }); const { code, json } = await plugin.run(['prompt', '--name', '../package.json']); assert.equal(code, 1); assert.equal(json.error.code, 'BAD_PROMPT_NAME'); }); test('plugin 目錄不完整時回既有的 PLUGIN_LAYOUT_BROKEN', async (t) => { const plugin = makeFakePlugin(t, { omit: ['templates'] }); const { code, json } = await plugin.run(['prompt', '--name', 'sdlc-plan']); assert.equal(code, 1); assert.equal(json.error.code, 'PLUGIN_LAYOUT_BROKEN'); assert.match(json.error.message, /templates/); }); // ── 轉接檔版本比對 ───────────────────────────────────────────────── test('版本相符時的輸出與不帶該 flag 時完全一致', async (t) => { const text = fakePrompt('sdlc-plan'); const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': text }, version: '9.9.9' }); const matched = await plugin.run(['prompt', '--name', 'sdlc-plan', '--adapter-version', '9.9.9']); const plain = await plugin.run(['prompt', '--name', 'sdlc-plan']); assert.equal(matched.raw, plain.raw); assert.equal(matched.raw, text); }); test('版本不符時第一行是重跑 install 的警告,其後逐字等於正本原文', async (t) => { const text = fakePrompt('sdlc-plan'); const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': text }, version: '9.9.9' }); const { code, raw } = await plugin.run([ 'prompt', '--name', 'sdlc-plan', '--adapter-version', '0.0.1', ]); // 正本仍然要給:過時的轉接檔不該讓流程停擺 assert.equal(code, 0); const [first, ...rest] = raw.split('\n'); assert.match(first, /tea-sdlc install/); assert.match(first, /0\.0\.1/); assert.match(first, /9\.9\.9/); assert.equal(rest.join('\n').trimStart(), text); assert.ok(raw.endsWith(text)); }); test('未帶 --adapter-version 時不產生任何警告', async (t) => { const text = fakePrompt('sdlc-plan'); const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': text } }); const { raw } = await plugin.run(['prompt', '--name', 'sdlc-plan']); assert.equal(raw.includes('tea-sdlc install'), false); }); test('比對用的版本只有 manifest 一個來源,沒有另外寫死一份', async (t) => { const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': fakePrompt('sdlc-plan') }, version: '7.7.7', }); const { raw } = await plugin.run([ 'prompt', '--name', 'sdlc-plan', '--adapter-version', version(), ]); // 假根的版本被改過,帶上真 repo 的版本就該被判為不符 assert.match(raw.split('\n')[0], /7\.7\.7/); });