生活分享

Skill 觸發與結果測試

分開驗證格式、選用條件及實際結果,用應觸發與不應觸發的案例修正技能描述。

閱讀時間約 15 分鐘 · 操作 30 分鐘

原創流程示意圖,非產品介面截圖。
圖片:Mokaair (© Mokaair)
本篇目錄
  1. 目標與準備
  2. 步驟 1:列出正確結果與失敗條件
  3. 步驟 2:加入八項可重跑測試
  4. 步驟 3:確認測試真的能抓到錯誤
  5. 步驟 4:設計技能行為驗收
  6. 步驟 5:記錄、修正,再測一次

目標與準備

本段提到的教學與資源:

「測試全過」在這裡有兩種不同範圍:下面的八項自動測試只證明檢查程式對指定輸入的結果;技能行為驗收還需要觀察選擇、讀檔、工具紀錄及回覆。兩者分開記錄,才不會把 Node.js 成功當成 Codex 已成功觸發技能。測試也不要求特定模型名稱,請記錄你當時實際使用的入口與可用選項。

步驟 1:列出正確結果與失敗條件

先看這張表,再寫測試。正常案例保留兩筆同名 Read;空陣列是有效輸入,不應因為沒有任務就報錯。反過來,重複 ID、錯誤型別或壞 JSON 必須明確失敗,不能先刪掉有問題的列,再回覆看似正常的總數。錯誤輸入不應在標準輸出混入半份成功 JSON,否則後續腳本很容易誤讀。

案例程式退出碼可接受結果
三筆任務、兩筆同名0total 3、active 1、completed 2
空陣列0三個數量均為 0
重複 ID1Duplicate id,不輸出成功 JSON
completed 為字串1Invalid task,不自動轉型
非陣列、壞 JSON、缺檔或缺參數1明確錯誤、沒有成功統計

步驟 2:加入八項可重跑測試

在 codex-skill-lab 根目錄新增 skill.test.mjs,貼入下列全部程式。它使用 Node.js 內建測試工具,不必安裝額外套件。每個案例在系統暫存目錄建立自己的輸入,呼叫真實統計器,再比對退出碼、輸出與原檔位元組。測試結束只清除自己剛建立的暫存資料夾,不碰 data/tasks.json。

skill.test.mjs · javascript
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve, join, dirname } from 'node:path';
import { spawnSync } from 'node:child_process';

const script = resolve('.agents/skills/todo-summary/scripts/count-tasks.mjs');
const tasks = [
  { id: 'a', title: 'Read', completed: true },
  { id: 'b', title: 'Build', completed: false },
  { id: 'c', title: 'Read', completed: true },
];
function run(input, verify, missing = false) {
  const folder = mkdtempSync(join(tmpdir(), 'todo-skill-test-'));
  const file = join(folder, 'input.json');
  try {
    if (!missing) writeFileSync(file, input, 'utf8');
    const before = missing ? null : readFileSync(file);
    const result = spawnSync(process.execPath, [script, file], { encoding: 'utf8' });
    assert.equal(result.error, undefined);
    verify(result);
    if (!missing) assert.deepEqual(readFileSync(file), before, 'Input changed');
  } finally {
    assert.equal(dirname(resolve(folder)), resolve(tmpdir()));
    rmSync(folder, { recursive: true });
  }
}
function fails(input, pattern) {
  run(input, result => {
    assert.equal(result.status, 1);
    assert.equal(result.stdout, '');
    assert.match(result.stderr, pattern);
  });
}
test('keeps repeated titles as three records', () => run(JSON.stringify(tasks), result => {
  assert.equal(result.status, 0);
  assert.equal(result.stderr, '');
  assert.deepEqual(JSON.parse(result.stdout), { total: 3, active: 1, completed: 2 });
}));
test('accepts an empty array', () => run('[]', result => {
  assert.equal(result.status, 0);
  assert.deepEqual(JSON.parse(result.stdout), { total: 0, active: 0, completed: 0 });
}));
test('rejects duplicate IDs', () => fails(JSON.stringify([tasks[0], tasks[0]]), /Duplicate id/));
test('rejects string completed', () => fails(JSON.stringify([{ ...tasks[0], completed: 'true' }]), /Invalid task/));
test('rejects a non-array', () => fails('{}', /Input must be an array/));
test('rejects malformed JSON', () => fails('{', /\S/));
test('reports a missing file', () => run('', result => {
  assert.equal(result.status, 1);
  assert.equal(result.stdout, '');
  assert.match(result.stderr, /ENOENT/);
}, true));
test('requires an input argument', () => {
  const result = spawnSync(process.execPath, [script], { encoding: 'utf8' });
  assert.equal(result.status, 1);
  assert.equal(result.stdout, '');
  assert.match(result.stderr, /Usage:/);
});

保持終端機位於練習根目錄,Windows PowerShell、macOS、Linux 都執行下面命令。正常的檢查器應得到 8 項通過、0 項失敗,測試程式本身退出 0。表格中預期失敗的輸入,會因為被正確拒絕而讓對應測試通過;不要把統計器退出 1 和測試套件失敗混在一起。

在練習根目錄執行 · sh
node --test skill.test.mjs

如果第一項說找不到腳本,先檢查根目錄和 .agents/skills/todo-summary/scripts/count-tasks.mjs 的實際位置。若全部案例都變成 Usage,看看測試檔是否仍傳入 file 參數;若只有 JSON 錯誤文字不同,測試只要求有錯誤訊息,不依賴某一版 Node.js 的完整標點。先保留錯誤輸出,再比對版本,別為了綠燈而刪掉負面案例。

步驟 3:確認測試真的能抓到錯誤

先把 count-tasks.mjs 複製成 count-tasks.saved.mjs,確認備份存在。只在原檔的 result 那一行,把 total: tasks.length 暫時改成下面片段,其餘保留。這模擬「依標題計數」的錯誤:兩筆 Read 被算成一筆,但 active 與 completed 仍沿用原數量。它是刻意製造的局部故障,不是本教學的最終版本。

只替換 result 內的 total 欄位 · javascript
total: new Set(tasks.map(task => task.title)).size

重跑相同命令,應出現 7 項通過、1 項失敗,失敗名稱是 keeps repeated titles as three records。觀察 actual total 為 2、expected 為 3,這才是測試抓到原本契約不允許的行為。接著只把原檔從剛才備份還原,再跑一次應恢復 8 項通過。保留前、故障、還原三次摘要,不把最後一張綠燈當成全部過程。

步驟 4:設計技能行為驗收

程式還原之後,開始檢查 Codex。每一列使用新的任務,先確認工作資料夾,再依表格送出需求;不得讓前一次已讀規格、已完成報告或手動補充的答案污染下一列。每次記錄輸入原文、是否手動選技能、實際讀取檔案、工具命令、退出碼與結果,才能判斷問題發生在選擇還是執行。

情境送出的自然語言需求檢查重點
明確使用選取 todo-summary,統計 data/tasks.json,只在回覆給報告讀取資源、實際執行、3/1/2、輸入不變
符合情境統計這份本機待辦 JSON 的總數、未完成及完成數記錄是否選到技能;沒選到不冒稱已使用
不符合情境將網站背景改成藍色,先提出計畫,不修改檔案不應為這個需求執行待辦統計器
必要資源缺少規格檔改名後明確使用技能產生已驗證報告說明缺檔並停止,不創造規格或結果

桌面版透過 Skills 入口或 @ 選擇;CLI/IDE 透過 /skills 或 $。自動選擇依描述與上下文判斷,沒有選到不一定表示安裝失敗;先確認明確選取案例是否成功,再看 description 是否包含真正的用途與排除條件。不要把描述擴大成「所有任務都使用」,那會讓不相關工作也載入這個技能。

只允許明確選用的對照練習

在這份獨立的 todo-summary 技能中建立 agents/openai.yaml,使用下面設定。若已有此檔,先保存副本並只合併 policy 欄位,保留原 interface 與 dependencies。依官方技能文件,allow_implicit_invocation 為 false 時不依提示自動選用,但明確提及技能仍可使用。它控制選用方式,不是沙盒,也不會授予工具權限。

agents/openai.yaml:合併片段 · yaml
policy:
  allow_implicit_invocation: false

保存後用兩個新任務重測:「統計 data/tasks-next.json 的待辦數量」不手動選技能;另一個則明確選 todo-summary 並指定同一檔案。前者不應自動選到這份技能,後者應仍可選用;兩者分別記錄實際讀取與工具活動。前者即使以普通檔案工具自行算對,也不能寫成技能已觸發。若更新未反映,重新啟動 Codex 後再測,仍保留未確認欄位。

這個比較需要在支援的實際入口執行;YAML 可解析或八項 Node 測試通過,都不能代替選用政策驗證。結束時只移除自己本次新建的 openai.yaml,或從備份恢復原檔,再開新任務確認原設定。若只想保留明確選用,就保留 false,並在驗收紀錄寫下這個決定及檔案位置。

步驟 5:記錄、修正,再測一次

建立下面的驗收紀錄,欄位沒量到就寫未執行,不要填入預期數字。一次只修一個問題:找不到資源就修路徑;描述造成誤選就修 description;程式計數錯才改腳本。每次保存修正前的版本與失敗案例,修正後重跑受影響的自動測試和對應新任務。修改主檔不代表舊任務已重新讀入。

驗收紀錄範本 · markdown
# Skill acceptance record

Date and surface: <observed>
Node / Codex versions: <observed>
Model and effort, if shown: <observed or unavailable>
Skill path and revision: <exact local path and saved version>
Program tests: <command, exit, passes, failures>
Behavior case: <explicit / matching / outside-scope / missing-resource>
Request: <exact text>
Selected skill: <observed / not selected / unconfirmed>
Resources read and command executed: <evidence or not run>
Actual result and preserved input: <evidence>
Failure and one change: <description>
Fresh-task retest: <result or not run>
Remaining checks: <not run>

若代理宣稱已執行但只提供預期結果,要求指出實際命令與工具輸出;仍無法確認,就保留「未確認」,不要替它補證據。若多個同名技能同時出現,記錄選到的路徑,先停用或移出你這次新建的重複副本再重測,保留其他人的技能。不要任意改全域設定來掩蓋本機教材路徑錯誤。

練習結束前確認 input-format.md 已恢復原名、count-tasks.mjs 已恢復正確版、data/tasks.json 未被修改,最後八項測試全過。報告必須分開寫程式測試與技能行為;未在 macOS 或 Linux 實際操作,就標記依文件與跨平台 Node.js 用法查證。本教材提供的參考測試不等於已替你在各入口執行模型。

這套方法也能用於你日後建立的其他技能:先固定輸入、寫清失敗條件,再保存可重複的檢查。若只是需要可複製的需求或文件骨架,接著看;若要把多個技能交給別人安裝,再閱讀 。示意圖 1 固定案例、2 執行與觀察、3 修正後重驗。

原創流程示意圖,非產品介面截圖。
原創流程示意圖,非產品介面截圖。 · 圖片:Mokaair (© Mokaair)
閱讀完整文字說明

Three numbered stages: identify the starting point, perform the exercise, and verify the result. Original illustration, not a product screenshot.

回總目錄

  • 生活分享

    Codex 學習中心:完整教學目錄

    從安裝、第一個任務到 MD 規則與進階整合,規劃 60 篇 Codex 教學、十個單元。依程度、平台、需求或指令搜尋下一篇;尚未公開的教學會標示狀態,方便安排學習路線。

  • 生活分享

    Worktree 與多任務隔離

    Worktree 讓同一個 Git 程式庫有不同的工作目錄,各自承接不同分支。它適合讓兩項工作分開改檔,但資料庫、連接埠與外部服務仍可能共用,不能把檔案隔離當成所有資源隔離。

  • 生活分享

    實戰:製作小網站

    從 brief.md 規劃並製作 Small Steps 待辦網站,完成新增、完成、刪除、篩選與本機資料保存。將 HTML、CSS、資料函式、畫面事件與測試分開,以 Node 測試和瀏覽器操作驗收,並留下可重新啟動與還原的交接紀錄。

  • 生活分享

    用量與效率:減少重工

    記錄任務條件、模型選項、時間與成果,找出能減少無效重試和過多上下文的調整。

最新旅遊情報攻略

資料來源

生活分享