mirror of
https://github.com/Heretek-AI/GDevelop-Unlocked.git
synced 2026-11-07 10:55:58 -10:00
Merge ticket-T04 (T04 No-Project Fix #21) into integration/v0.2
This commit is contained in:
commit
1d884d12c2
5 files changed
+211
-27
No files matched your search
@@ -3090,6 +3090,13 @@ Guidelines:
|
||||
|
||||
`;
|
||||
|
||||
// T04 (#21): when no project JSON is present, no project is open. State the
|
||||
// self-remedy explicitly so the model's first project-dependent call is
|
||||
// `initialize_project` instead of a dead-end null failure.
|
||||
if (!gameProjectJson) {
|
||||
prompt += `\nNo project is currently open. You must call initialize_project(template_slug:'empty') to create one before using project tools.\n`;
|
||||
}
|
||||
|
||||
if (gameProjectJson) {
|
||||
prompt += `\nCurrent Project Structure:\n${gameProjectJson}\n`;
|
||||
}
|
||||
@@ -4014,6 +4021,39 @@ export const LOCAL_PLAN_TOOL_NAMES: Array<string> = ['create_or_update_plan'];
|
||||
export const isLocalPlanTool = (toolName: string): boolean =>
|
||||
LOCAL_PLAN_TOOL_NAMES.includes(toolName);
|
||||
|
||||
/**
|
||||
* T04 (#21) no-project self-remedy: project-dependent tools called with
|
||||
* `project === null` (no project open) fail deterministically with a
|
||||
* remedy the model can act on — calling `initialize_project` — instead of
|
||||
* a dead-end null crash or a silent no-op. Routed by the Utils.js pre-pass
|
||||
* before the editor dispatch; also surfaced in the system prompt when no
|
||||
* project JSON is present.
|
||||
*/
|
||||
export const NO_PROJECT_MESSAGE: string =
|
||||
"No project is currently open. You must call initialize_project(template_slug:'empty') to create one before using project tools.";
|
||||
|
||||
export const PROJECT_FREE_TOOL_NAMES: Array<string> = [
|
||||
'initialize_project',
|
||||
'get_game_starter_summary',
|
||||
'search_docs',
|
||||
'read_full_docs',
|
||||
'search_object_asset_store',
|
||||
'search_resource_store',
|
||||
'create_or_update_plan',
|
||||
'spawn_agent',
|
||||
];
|
||||
|
||||
export const isProjectDependentTool = (toolName: string): boolean =>
|
||||
typeof toolName === 'string' &&
|
||||
toolName.length > 0 &&
|
||||
!PROJECT_FREE_TOOL_NAMES.includes(toolName);
|
||||
|
||||
export const buildNoProjectResult = (toolName: string): Object => ({
|
||||
success: false,
|
||||
noProject: true,
|
||||
message: `${NO_PROJECT_MESSAGE} (tool: ${toolName})`,
|
||||
});
|
||||
|
||||
export const withoutBackendOnlyTools = (tools: Array<Object>): Array<Object> =>
|
||||
tools.filter(
|
||||
tool =>
|
||||
|
||||
@@ -94,6 +94,7 @@ export const STUDIO_ROLES: { [StudioRoleId]: StudioRole } = {
|
||||
readOnly: false,
|
||||
maxTurns: 60,
|
||||
allowedToolNames: [
|
||||
'initialize_project',
|
||||
'create_or_update_plan',
|
||||
'spawn_agent',
|
||||
'read_game_project_json',
|
||||
@@ -110,6 +111,8 @@ export const STUDIO_ROLES: { [StudioRoleId]: StudioRole } = {
|
||||
],
|
||||
systemPrompt: `You are the studio lead of a small game studio building a game in GDevelop.
|
||||
|
||||
If no project is currently open, your first action must be initialize_project with template_slug 'empty' (or the requested template) before planning or delegating. A plan or spawn without a project cannot succeed.
|
||||
|
||||
Your job is to plan and delegate. You never edit the project yourself - you have no tool to do so.
|
||||
|
||||
Follow this loop, every time:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"files": {
|
||||
"AI/CustomAIClient.js": {
|
||||
"bytes": 210819,
|
||||
"bytes": 212395,
|
||||
"origin": "newIDE/app/src/AI/CustomAIClient.js",
|
||||
"sha256": "0934ccb0060cf2c5607ecb192d855189d3fdc679545ee41ad748cd81f642a64b"
|
||||
"sha256": "28e24dde3cde3941f6ae514d415fcc5069ce2229f3d888e95aa767ebef7e22f1"
|
||||
},
|
||||
"AI/CustomAIClient.spec.js": {
|
||||
"bytes": 257794,
|
||||
@@ -51,9 +51,9 @@
|
||||
"sha256": "66a7d577f12fccaa1b23d89bc7ba02c9bca07058358cc53d3ecfe50d604ef1b8"
|
||||
},
|
||||
"AiGeneration/Studio/Roles.js": {
|
||||
"bytes": 12493,
|
||||
"bytes": 12736,
|
||||
"origin": "newIDE/app/src/AiGeneration/Studio/Roles.js",
|
||||
"sha256": "dac71987229c63fcea55fabd049b43ab48beff68cb19813de9d1c77090c6798f"
|
||||
"sha256": "1ebb998a8038a964151d28b5af6c4d4fdd4dffee2d563d0b83e9964ba13f8457"
|
||||
},
|
||||
"AiGeneration/Studio/SafeTruncation.js": {
|
||||
"bytes": 2743,
|
||||
|
||||
+67
-23
@@ -17,7 +17,7 @@
|
||||
} from './AiRequestUtils';
|
||||
import { useEnsureExtensionInstalled } from './UseEnsureExtensionInstalled';
|
||||
import { useGenerateEvents } from './UseGenerateEvents';
|
||||
@@ -62,6 +63,37 @@
|
||||
@@ -62,6 +63,39 @@
|
||||
import { makeSimplifiedProjectBuilder } from '../EditorFunctions/SimplifiedProject/SimplifiedProject';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../Utils/GDevelopServices/Errors';
|
||||
@@ -27,6 +27,8 @@
|
||||
+ isOfflineDocsTool,
|
||||
+ OFFLINE_DOCS_MESSAGE,
|
||||
+ isLocalPlanTool,
|
||||
+ isProjectDependentTool,
|
||||
+ NO_PROJECT_MESSAGE,
|
||||
+} from '../AI/CustomAIClient';
|
||||
+import {
|
||||
+ isSpawnAgentCall,
|
||||
@@ -55,7 +57,7 @@
|
||||
import UnsavedChangesContext from '../MainFrame/UnsavedChangesContext';
|
||||
import {
|
||||
type FileMetadata,
|
||||
@@ -213,12 +245,10 @@
|
||||
@@ -213,12 +247,10 @@
|
||||
editorCallbacks: EditorCallbacks,
|
||||
|}): React.Node => {
|
||||
if (aiRequest.parentAiRequestId) {
|
||||
@@ -72,7 +74,7 @@
|
||||
if (launchingCall) {
|
||||
return renderFunctionCallLabel({
|
||||
functionCall: launchingCall,
|
||||
@@ -244,6 +274,7 @@
|
||||
@@ -244,6 +276,7 @@
|
||||
editorCallbacks,
|
||||
aiRequestsToProcess,
|
||||
onSendEditorFunctionCallResults,
|
||||
@@ -80,7 +82,7 @@
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
@@ -265,6 +296,9 @@
|
||||
@@ -265,6 +298,9 @@
|
||||
suspendAiRequest,
|
||||
requestEditApproval,
|
||||
onSaveProjectAsWithStorageProvider,
|
||||
@@ -90,7 +92,7 @@
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
project: ?gdProject,
|
||||
@@ -280,7 +314,7 @@
|
||||
@@ -280,7 +316,7 @@
|
||||
createdExternalLayoutNames?: Array<string>,
|
||||
createdProject?: ?gdProject,
|
||||
|}
|
||||
@@ -99,7 +101,7 @@
|
||||
getEditorFunctionCallResults: string => Array<EditorFunctionCallResult> | null,
|
||||
addEditorFunctionCallResults: (
|
||||
string,
|
||||
@@ -319,6 +353,9 @@
|
||||
@@ -319,6 +355,9 @@
|
||||
onWillInstallExtension: (extensionNames: Array<string>) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
isReadyToProcessFunctionCalls: boolean,
|
||||
@@ -109,7 +111,7 @@
|
||||
getIsAutoEditEnabled: () => boolean,
|
||||
suspendAiRequest: (aiRequestId: string) => Promise<void>,
|
||||
requestEditApproval: (request: EditApprovalRequest) => Promise<boolean>,
|
||||
@@ -331,12 +368,30 @@
|
||||
@@ -331,12 +370,30 @@
|
||||
createdProject?: gdProject,
|
||||
|}
|
||||
) => Promise<?FileMetadata>,
|
||||
@@ -140,7 +142,7 @@
|
||||
} => {
|
||||
const { ensureExtensionInstalled } = useEnsureExtensionInstalled({
|
||||
project,
|
||||
@@ -424,6 +479,14 @@
|
||||
@@ -424,6 +481,14 @@
|
||||
// `call:<callId>` (for a single direct modifying call like generate_events).
|
||||
const approvedEditBatchKeysRef = React.useRef<Set<string>>(new Set());
|
||||
|
||||
@@ -155,7 +157,7 @@
|
||||
// Forget all previously-granted edit approvals so the next modifying call
|
||||
// asks again. Called when the user toggles auto-edit: turning it on then off
|
||||
// again means they want to review the upcoming edits, even within a sub-agent
|
||||
@@ -440,6 +503,16 @@
|
||||
@@ -440,6 +505,16 @@
|
||||
if (!isReadyToProcessFunctionCalls) return;
|
||||
if (aiRequest.status === 'suspended') return;
|
||||
|
||||
@@ -172,7 +174,7 @@
|
||||
const functionCallsToProcess = functionCalls.filter(
|
||||
functionCall =>
|
||||
!inFlightFunctionCallIdsRef.current.has(
|
||||
@@ -460,14 +533,60 @@
|
||||
@@ -460,14 +535,60 @@
|
||||
);
|
||||
});
|
||||
|
||||
@@ -235,7 +237,7 @@
|
||||
|
||||
// Gate project-modifying calls behind a user confirmation when auto-edit
|
||||
// is off. Read-only calls (exploration, inspection) always run. The first
|
||||
@@ -493,10 +612,16 @@
|
||||
@@ -493,10 +614,16 @@
|
||||
const modifyingFunctionCalls = functionCallsToProcess.filter(
|
||||
functionCall =>
|
||||
doesFunctionCallModifyProject(functionCall) &&
|
||||
@@ -252,7 +254,7 @@
|
||||
!isCallApproved(functionCall)
|
||||
);
|
||||
|
||||
@@ -554,13 +679,365 @@
|
||||
@@ -554,13 +681,406 @@
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +365,44 @@
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // T04 (#21): no-project self-remedy. With no project open every
|
||||
+ // project-dependent tool fails deterministically with the
|
||||
+ // initialize_project remedy instead of a null crash. Checked before
|
||||
+ // the editor dispatch; initialize_project itself is never blocked.
|
||||
+ const noProjectHandledCallIds = new Set<string>();
|
||||
+ const noProjectResults: Array<EditorFunctionCallResult> = [];
|
||||
+ if (isCustomEndpointEnabled() && !project) {
|
||||
+ for (const functionCall of functionCallsToProcess) {
|
||||
+ if (studioDeniedCallIds.has(functionCall.call_id)) continue;
|
||||
+ if (loopGuardHandledCallIds.has(functionCall.call_id)) continue;
|
||||
+ const needsProject =
|
||||
+ typeof isProjectDependentTool === 'function'
|
||||
+ ? isProjectDependentTool(functionCall.name)
|
||||
+ : functionCall.name !== 'initialize_project' &&
|
||||
+ functionCall.name !== 'get_game_starter_summary' &&
|
||||
+ functionCall.name !== 'search_docs' &&
|
||||
+ functionCall.name !== 'read_full_docs' &&
|
||||
+ functionCall.name !== 'search_object_asset_store' &&
|
||||
+ functionCall.name !== 'search_resource_store' &&
|
||||
+ functionCall.name !== 'create_or_update_plan' &&
|
||||
+ functionCall.name !== 'spawn_agent';
|
||||
+ if (needsProject) {
|
||||
+ noProjectHandledCallIds.add(functionCall.call_id);
|
||||
+ const noProjectDenial = {
|
||||
+ status: 'finished',
|
||||
+ call_id: functionCall.call_id,
|
||||
+ success: false,
|
||||
+ output: {
|
||||
+ message: `${NO_PROJECT_MESSAGE} (tool: ${functionCall.name})`,
|
||||
+ noProject: true,
|
||||
+ },
|
||||
+ };
|
||||
+ noProjectResults.push(noProjectDenial);
|
||||
+ addEditorFunctionCallResults(aiRequest.id, [noProjectDenial]);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // T03 (#24): offline docs + local plan pre-pass. Both are dead upstream
|
||||
+ // (EditorFunctions fails them as "handled server-side"), so they are
|
||||
+ // routed locally with zero network I/O before the editor dispatch. A
|
||||
@@ -376,6 +416,7 @@
|
||||
+ for (const functionCall of functionCallsToProcess) {
|
||||
+ if (studioDeniedCallIds.has(functionCall.call_id)) continue;
|
||||
+ if (loopGuardHandledCallIds.has(functionCall.call_id)) continue;
|
||||
+ if (noProjectHandledCallIds.has(functionCall.call_id)) continue;
|
||||
+ const isDocs =
|
||||
+ functionCall.name === 'search_docs' ||
|
||||
+ functionCall.name === 'read_full_docs' ||
|
||||
@@ -578,7 +619,8 @@
|
||||
+ !loopGuardHandledCallIds.has(functionCall.call_id) &&
|
||||
+ !studioDeniedCallIds.has(functionCall.call_id) &&
|
||||
+ !offlineDocsHandledCallIds.has(functionCall.call_id) &&
|
||||
+ !localPlanHandledCallIds.has(functionCall.call_id)
|
||||
+ !localPlanHandledCallIds.has(functionCall.call_id) &&
|
||||
+ !noProjectHandledCallIds.has(functionCall.call_id)
|
||||
+ );
|
||||
+ // Mark only the editor calls as "working": a spawn (or a loop-guard hit)
|
||||
+ // already has a terminal result, and a permanent "working" entry would
|
||||
@@ -601,6 +643,7 @@
|
||||
+ ...spawnFailureResults,
|
||||
+ ...offlineDocsResults,
|
||||
+ ...localPlanResults,
|
||||
+ ...noProjectResults,
|
||||
+ ];
|
||||
+ if (outcomeResults.length > 0) {
|
||||
+ await onSendEditorFunctionCallResults(
|
||||
@@ -619,7 +662,7 @@
|
||||
|
||||
// The "modified outside editor" callbacks each refresh the editor and can
|
||||
// trigger an in-game editor hot reload. Firing them once per function
|
||||
@@ -669,7 +1146,7 @@
|
||||
@@ -669,7 +1189,7 @@
|
||||
// Explorer sub-agent scripts are read-only: restrict their
|
||||
// `run_script` to non-mutating functions (defense in depth).
|
||||
runScriptReadOnly: isReadOnlyScriptContext,
|
||||
@@ -628,7 +671,7 @@
|
||||
name: functionCall.name,
|
||||
arguments: functionCall.arguments,
|
||||
call_id: functionCall.call_id,
|
||||
@@ -761,13 +1238,54 @@
|
||||
@@ -761,13 +1281,55 @@
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -674,6 +717,7 @@
|
||||
+ ...spawnFailureResults,
|
||||
+ ...offlineDocsResults,
|
||||
+ ...localPlanResults,
|
||||
+ ...noProjectResults,
|
||||
+ ...mergedResults,
|
||||
+ ],
|
||||
+ {
|
||||
@@ -688,7 +732,7 @@
|
||||
} finally {
|
||||
// Flush the coalesced editor notifications for everything modified in
|
||||
// this batch (one hot reload instead of one per call). In `finally` so
|
||||
@@ -787,7 +1305,9 @@
|
||||
@@ -787,7 +1349,9 @@
|
||||
}
|
||||
|
||||
// Release the lock so these calls can be retried if needed
|
||||
@@ -699,7 +743,7 @@
|
||||
functionCallsToProcess.forEach(functionCall => {
|
||||
inFlightFunctionCallIdsRef.current.delete(
|
||||
`${aiRequest.id}:${functionCall.call_id}`
|
||||
@@ -824,9 +1344,12 @@
|
||||
@@ -824,9 +1388,12 @@
|
||||
getAssetStoreTagForNewObject,
|
||||
generateEvents,
|
||||
onSendEditorFunctionCallResults,
|
||||
@@ -712,7 +756,7 @@
|
||||
]
|
||||
);
|
||||
|
||||
@@ -870,9 +1393,50 @@
|
||||
@@ -870,9 +1437,50 @@
|
||||
[onProcessFunctionCalls, allFunctionCallsToProcess]
|
||||
);
|
||||
|
||||
@@ -763,7 +807,7 @@
|
||||
};
|
||||
};
|
||||
|
||||
@@ -977,7 +1541,6 @@
|
||||
@@ -977,7 +1585,6 @@
|
||||
editorFunctionCallResultsStorage,
|
||||
isFetchingSuggestions,
|
||||
setIsFetchingSuggestions,
|
||||
@@ -771,7 +815,7 @@
|
||||
setSelectedAiRequestId,
|
||||
selectedAiRequest,
|
||||
} = React.useContext(AiRequestContext);
|
||||
@@ -1036,20 +1599,19 @@
|
||||
@@ -1036,20 +1643,19 @@
|
||||
// Then ask for some.
|
||||
if (
|
||||
!selectedAiRequest ||
|
||||
@@ -803,7 +847,7 @@
|
||||
|
||||
// Check if there are tools being run. If so, no suggestions at this time.
|
||||
const hasFunctionsCallsToProcess =
|
||||
@@ -1139,7 +1701,7 @@
|
||||
@@ -1139,7 +1745,7 @@
|
||||
: null;
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
@@ -812,7 +856,7 @@
|
||||
simplifiedProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
eventsJson: null,
|
||||
@@ -1153,7 +1715,7 @@
|
||||
@@ -1153,7 +1759,7 @@
|
||||
const aiRequestWorkingForSuggestions = await getAiRequestSuggestions(
|
||||
getAuthorizationHeader,
|
||||
{
|
||||
@@ -821,7 +865,7 @@
|
||||
aiRequestId: selectedAiRequest.id,
|
||||
suggestionsType: isLastMessageFunctionCallOutputProjectInitialization
|
||||
? 'list-with-explanations'
|
||||
@@ -1525,39 +2087,12 @@
|
||||
@@ -1525,39 +2131,12 @@
|
||||
[profile, setSelectedAiRequestId]
|
||||
);
|
||||
|
||||
|
||||
@@ -488,3 +488,100 @@ describe('integration: T03 tool manifest pruning (no dead tool calls)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration: T04 no-project self-remedy (null project)', () => {
|
||||
it('project-dependent tools fail closed with the initialize_project remedy', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const { fileURLToPath } = await import('node:url');
|
||||
const { dirname, resolve } = await import('node:path');
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const src = readFileSync(
|
||||
resolve(root, 'packages/ai-studio/AI/CustomAIClient.js'),
|
||||
'utf8'
|
||||
);
|
||||
// Deterministic self-remedying error text (exact match on the contract).
|
||||
assert.ok(src.includes('NO_PROJECT_MESSAGE'), 'NO_PROJECT_MESSAGE export missing');
|
||||
assert.ok(
|
||||
src.includes("No project is currently open. You must call initialize_project(template_slug:'empty')"),
|
||||
'no-project remedy text missing or reworded'
|
||||
);
|
||||
assert.ok(src.includes('isProjectDependentTool'), 'isProjectDependentTool helper missing');
|
||||
assert.ok(src.includes('buildNoProjectResult'), 'buildNoProjectResult helper missing');
|
||||
assert.ok(src.includes('PROJECT_FREE_TOOL_NAMES'), 'PROJECT_FREE_TOOL_NAMES missing');
|
||||
const helperAt = src.indexOf('export const buildNoProjectResult');
|
||||
assert.ok(helperAt !== -1, 'buildNoProjectResult export missing');
|
||||
const helperSlice = src.slice(helperAt, helperAt + 2000);
|
||||
assert.ok(helperSlice.includes('success: false'), 'no-project result must be success:false');
|
||||
assert.ok(helperSlice.includes('noProject: true'), 'no-project result must set noProject:true');
|
||||
assert.ok(!helperSlice.includes('fetch('), 'no-project result must not fetch');
|
||||
// initialize_project itself is never project-dependent (it IS the remedy).
|
||||
const freeAt = src.indexOf('PROJECT_FREE_TOOL_NAMES');
|
||||
const freeSlice = src.slice(freeAt, freeAt + 1200);
|
||||
assert.ok(freeSlice.includes("'initialize_project'"), 'initialize_project must be project-free');
|
||||
// System prompt states the remedy when no project JSON is present.
|
||||
assert.ok(
|
||||
src.includes("No project is currently open. You must call initialize_project(template_slug:'empty') to create one before using project tools."),
|
||||
'buildSystemPrompt must state the no-project remedy'
|
||||
);
|
||||
});
|
||||
|
||||
it('manager role leads with initialize_project when no project is open', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const { fileURLToPath } = await import('node:url');
|
||||
const { dirname, resolve } = await import('node:path');
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const roles = readFileSync(
|
||||
resolve(root, 'packages/ai-studio/AiGeneration/Studio/Roles.js'),
|
||||
'utf8'
|
||||
);
|
||||
const managerAt = roles.indexOf('manager: {');
|
||||
assert.ok(managerAt !== -1, 'manager role missing');
|
||||
const managerSlice = roles.slice(managerAt, managerAt + 4000);
|
||||
// initialize_project prominent: first in the manager tool manifest.
|
||||
const initAt = managerSlice.indexOf("'initialize_project'");
|
||||
assert.ok(initAt !== -1, 'initialize_project missing from manager allowedToolNames');
|
||||
const planAt = managerSlice.indexOf("'create_or_update_plan'");
|
||||
assert.ok(planAt !== -1, 'manager plan tool missing');
|
||||
assert.ok(
|
||||
initAt < planAt,
|
||||
'initialize_project must precede create_or_update_plan in the manager manifest (prominent)'
|
||||
);
|
||||
// First-action rule in the manager prompt.
|
||||
assert.ok(
|
||||
managerSlice.includes('your first action must be initialize_project'),
|
||||
'manager prompt must order initialize_project first when no project is open'
|
||||
);
|
||||
});
|
||||
|
||||
it('patch 14 routes null-project calls to the self-remedy before the editor', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const { fileURLToPath } = await import('node:url');
|
||||
const { dirname, resolve } = await import('node:path');
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const patch14 = readFileSync(resolve(root, 'patches/14-ai-utils.patch'), 'utf8');
|
||||
for (const marker of [
|
||||
'noProjectHandledCallIds',
|
||||
'noProjectResults',
|
||||
'isProjectDependentTool',
|
||||
'NO_PROJECT_MESSAGE',
|
||||
]) {
|
||||
assert.ok(patch14.includes(marker), `patch 14 missing T04 marker: ${marker}`);
|
||||
}
|
||||
assert.ok(
|
||||
patch14.includes('isCustomEndpointEnabled() && !project'),
|
||||
'no-project pre-pass must gate on !project under BYOK'
|
||||
);
|
||||
assert.ok(
|
||||
patch14.includes('!noProjectHandledCallIds.has(functionCall.call_id)'),
|
||||
'no-project calls must be excluded from the editor dispatch'
|
||||
);
|
||||
assert.ok(
|
||||
patch14.includes('...noProjectResults,'),
|
||||
'no-project results must be sent to the model'
|
||||
);
|
||||
const preAt = patch14.indexOf('T04 (#21)');
|
||||
assert.ok(preAt !== -1, 'T04 pre-pass comment missing');
|
||||
const preSlice = patch14.slice(preAt, preAt + 4000);
|
||||
assert.ok(!preSlice.includes('fetch('), 'no-project pre-pass must not fetch');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user