When Codex reports `request_user_input` as unavailable (Default mode), the
Codex skill adapter's Execute-mode-fallback section now explicitly instructs
the agent to append `--text` to `{{GSD_ARGS}}` to activate the workflow's
built-in TEXT_MODE branching. This ensures every AskUserQuestion gate is
handled consistently through the workflow's own text-mode mechanism rather
than ad-hoc plain-text fallback, and eliminates any path to silent-default
selection (#3018 / #3808).
Adds regression test bug-3808-codex-adapter-text-mode-fallback.test.cjs with
typed semantic-flag assertions covering gsd-plan-phase, gsd-discuss-phase,
gsd-execute-phase, and gsd-verify-work.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
6.6 KiB
JavaScript
156 lines
6.6 KiB
JavaScript
/**
|
|
* Regression test for bug #3808.
|
|
*
|
|
* When Codex runs in Default mode, `request_user_input` is reported as
|
|
* unavailable. The Codex skill adapter must tell the agent to activate the
|
|
* workflow's built-in TEXT_MODE mechanism (`--text` flag) rather than either:
|
|
* (a) silently picking a default value — the #3018 failure mode, or
|
|
* (b) ad-hoc plain-text fallback that bypasses the workflow's own branching.
|
|
*
|
|
* Workflows (e.g. plan-phase.md) already have TEXT_MODE logic:
|
|
* "Set TEXT_MODE=true if `--text` is present in $ARGUMENTS OR text_mode
|
|
* from init JSON is true."
|
|
* The adapter must tell the agent to USE that mechanism when
|
|
* `request_user_input` is unavailable instead of inventing its own fallback
|
|
* or silently continuing with defaults.
|
|
*
|
|
* Test design: mirrors the typed-semantic-flag pattern from bug #3018 so that
|
|
* prose rewording doesn't break tests as long as the semantics stay correct.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
|
|
const INSTALL = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
|
const { getCodexSkillAdapterHeader } = INSTALL;
|
|
|
|
/**
|
|
* Extract the "Execute mode fallback" section text from the adapter header.
|
|
* Returns null if the section is missing. Section runs from the
|
|
* "Execute mode fallback:" label up to the next heading or </codex_skill_adapter> tag.
|
|
*/
|
|
function extractExecuteModeFallback(header) {
|
|
const m = header.match(/Execute mode fallback:\s*\n([\s\S]*?)(?=\n##\s|\n<\/codex_skill_adapter>)/);
|
|
return m ? m[1].trim() : null;
|
|
}
|
|
|
|
/**
|
|
* Parse the Execute-mode-fallback section into a typed semantic-flag record.
|
|
*
|
|
* Flags for bug #3808 (TEXT_MODE activation):
|
|
* activatesTextMode — does the prose tell the agent to activate TEXT_MODE / use --text?
|
|
* instructsStop — does the prose tell the agent to stop/halt/wait?
|
|
* presentsPlainText — does the prose mention plain-text / numbered-list presentation?
|
|
* silentlyPicksDefaults — (anti-pattern) does the prose instruct silent-default picking?
|
|
*/
|
|
function parseExecuteModeFallbackFor3808(section) {
|
|
if (!section || typeof section !== 'string') {
|
|
return {
|
|
ok: false,
|
|
sectionLength: 0,
|
|
activatesTextMode: false,
|
|
instructsStop: false,
|
|
presentsPlainText: false,
|
|
silentlyPicksDefaults: false,
|
|
};
|
|
}
|
|
|
|
const lower = section.toLowerCase();
|
|
|
|
// (a) TEXT_MODE activation — adapter must tell the agent to use the workflow's
|
|
// built-in text mode mechanism when request_user_input is unavailable.
|
|
// Accept either: explicit "--text" flag mention OR "text_mode" / "text mode"
|
|
// paired with context showing it is being SET/ACTIVATED (not just referenced).
|
|
const mentionsTextFlag = section.includes('--text');
|
|
const mentionsTextModeOn = /text_mode\s*=\s*true|set\s+text_mode|activate\s+text.?mode|enable\s+text.?mode|text.?mode.*active|text.?mode.*on\b/i.test(section);
|
|
const activatesTextMode = mentionsTextFlag || mentionsTextModeOn;
|
|
|
|
// (b) STOP/WAIT directive — the agent must halt instead of proceeding silently.
|
|
const instructsStop = /\b(stop|halt|wait)\b/.test(lower);
|
|
|
|
// (c) Plain-text fallback presentation.
|
|
const presentsPlainText = /plain.?text|numbered list/.test(lower);
|
|
|
|
// Anti-pattern guard — the prose that caused #3018 and resurfaces in #3808.
|
|
const silentlyPicksDefaults = /pick (a |the )?(reasonable|sensible|sane) default/i.test(section);
|
|
|
|
return {
|
|
ok: true,
|
|
sectionLength: section.length,
|
|
activatesTextMode,
|
|
instructsStop,
|
|
presentsPlainText,
|
|
silentlyPicksDefaults,
|
|
};
|
|
}
|
|
|
|
describe('bug #3808: codex skill adapter activates TEXT_MODE when request_user_input is unavailable', () => {
|
|
const SKILL_NAMES = ['gsd-plan-phase', 'gsd-discuss-phase', 'gsd-execute-phase', 'gsd-verify-work'];
|
|
|
|
test('getCodexSkillAdapterHeader is exported', () => {
|
|
assert.equal(typeof getCodexSkillAdapterHeader, 'function');
|
|
});
|
|
|
|
test('Execute mode fallback section exists for all key skills', () => {
|
|
for (const skillName of SKILL_NAMES) {
|
|
const header = getCodexSkillAdapterHeader(skillName);
|
|
const section = extractExecuteModeFallback(header);
|
|
assert.ok(section !== null && section.length > 0,
|
|
`${skillName}: Execute mode fallback section must exist and have content`);
|
|
}
|
|
});
|
|
|
|
for (const skillName of SKILL_NAMES) {
|
|
test(`${skillName}: fallback activates TEXT_MODE (--text flag or text_mode=true) when request_user_input is unavailable`, () => {
|
|
const header = getCodexSkillAdapterHeader(skillName);
|
|
const section = extractExecuteModeFallback(header);
|
|
const parsed = parseExecuteModeFallbackFor3808(section);
|
|
assert.equal(parsed.activatesTextMode, true,
|
|
`${skillName}: fallback must instruct the agent to activate TEXT_MODE (mention --text flag or text_mode=true/active) when request_user_input is unavailable (#3808). Section was:\n${section}`);
|
|
});
|
|
|
|
test(`${skillName}: fallback instructs STOP/WAIT (not silent continuation)`, () => {
|
|
const header = getCodexSkillAdapterHeader(skillName);
|
|
const section = extractExecuteModeFallback(header);
|
|
const parsed = parseExecuteModeFallbackFor3808(section);
|
|
assert.equal(parsed.instructsStop, true,
|
|
`${skillName}: fallback must include stop/halt/wait instruction. Section was:\n${section}`);
|
|
});
|
|
|
|
test(`${skillName}: fallback does NOT contain silent-default anti-pattern`, () => {
|
|
const header = getCodexSkillAdapterHeader(skillName);
|
|
const section = extractExecuteModeFallback(header);
|
|
const parsed = parseExecuteModeFallbackFor3808(section);
|
|
assert.equal(parsed.silentlyPicksDefaults, false,
|
|
`${skillName}: regression — fallback must NOT instruct the agent to pick defaults autonomously (#3018 / #3808). Section was:\n${section}`);
|
|
});
|
|
}
|
|
|
|
test('typed semantic-record snapshot for gsd-plan-phase — full contract', () => {
|
|
const section = extractExecuteModeFallback(getCodexSkillAdapterHeader('gsd-plan-phase'));
|
|
const parsed = parseExecuteModeFallbackFor3808(section);
|
|
assert.deepStrictEqual(
|
|
{
|
|
ok: parsed.ok,
|
|
activatesTextMode: parsed.activatesTextMode,
|
|
instructsStop: parsed.instructsStop,
|
|
presentsPlainText: parsed.presentsPlainText,
|
|
silentlyPicksDefaults: parsed.silentlyPicksDefaults,
|
|
},
|
|
{
|
|
ok: true,
|
|
activatesTextMode: true,
|
|
instructsStop: true,
|
|
presentsPlainText: true,
|
|
silentlyPicksDefaults: false,
|
|
},
|
|
`gsd-plan-phase: full TEXT_MODE fallback contract violated (#3808). Section was:\n${section}`,
|
|
);
|
|
});
|
|
});
|