Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,156 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => {
);
});

describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => {
const startTurnWithLaunchArgs = Effect.fnUntraced(function* (launchArgs: string, cwd: string) {
const fileSystem = yield* FileSystem.FileSystem;
const opened: Array<ClaudeAdapterV2.ClaudeAgentSdkQueryOptions> = [];
const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
settings: { ...DEFAULT_CLAUDE_SETTINGS, launchArgs },
environment: {},
attachmentsDir: yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-append-" }),
fileSystem,
path: yield* Path.Path,
crypto: yield* Crypto.Crypto,
idAllocator: yield* IdAllocator.IdAllocatorV2,
queryRunner: {
allocateSessionId: Effect.succeed("native-thread-claude-append-file"),
open: (input) =>
Effect.sync(() => {
opened.push(input.options);
return {
messages: Stream.never,
offer: () => Effect.void,
setModel: () => Effect.void,
applyFlagSettings: () => Effect.void,
setPermissionMode: () => Effect.void,
backgroundTasks: () => Effect.succeed(false),
stopTask: () => Effect.void,
interrupt: Effect.void,
close: Effect.void,
};
}),
forkSession: () => Effect.die("unused"),
subagentLaunchToolUseId: () => Effect.succeed(null),
assertComplete: Effect.void,
},
});
const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd,
});
const threadId = ThreadId.make("thread-claude-append-file");
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("provider-session-claude-append-file"),
modelSelection: CLAUDE_TEST_MODEL_SELECTION,
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection: CLAUDE_TEST_MODEL_SELECTION,
runtimePolicy,
});
const exit = yield* runtime
.startTurn(
makeClaudeTestTurnInput({
threadId,
providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("attempt-claude-append-file"),
text: "What is the sentinel?",
attachments: [],
runtimePolicy,
}),
)
.pipe(Effect.exit);
return { exit, opened };
});

const appendSystemPrompt = (options: ClaudeAdapterV2.ClaudeAgentSdkQueryOptions | undefined) =>
(options?.systemPrompt as { readonly append?: string } | undefined)?.append;

it.effect("sends append launch arguments to Claude after the T3 instructions", () =>
Effect.scoped(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-append-cwd-" });
yield* fileSystem.writeFileString(NodePath.join(cwd, "rules.md"), "File sentinel\n");
const plain = yield* startTurnWithLaunchArgs("", cwd);
const { exit, opened } = yield* startTurnWithLaunchArgs(
'--append-system-prompt "Inline sentinel" --append-system-prompt-file rules.md --system-prompt Replacement',
cwd,
);
assert.isTrue(Exit.isSuccess(exit));
assert.lengthOf(opened, 1);
assert.equal(
appendSystemPrompt(opened[0]),
`${appendSystemPrompt(plain.opened[0])}\n\nFile sentinel\n\nInline sentinel`,
);
assert.isUndefined(opened[0]!.extraArgs?.["append-system-prompt"]);
assert.isUndefined(opened[0]!.extraArgs?.["append-system-prompt-file"]);
assert.equal(opened[0]!.extraArgs?.["system-prompt"], "Replacement");
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
),
);

it.effect.each([
[
"--append-system-prompt-file missing.md",
(cwd: string) =>
new ClaudeAdapterV2.ClaudeAppendSystemPromptFileError({
path: NodePath.join(cwd, "missing.md"),
}).message,
],
[
"--append-system-prompt-file",
() =>
new ClaudeAdapterV2.ClaudeAppendSystemPromptMissingValueError({
flag: "--append-system-prompt-file",
}).message,
],
[
"--append-system-prompt-file=",
() =>
new ClaudeAdapterV2.ClaudeAppendSystemPromptMissingValueError({
flag: "--append-system-prompt-file",
}).message,
],
[
"--append-system-prompt",
() =>
new ClaudeAdapterV2.ClaudeAppendSystemPromptMissingValueError({
flag: "--append-system-prompt",
}).message,
],
] as const)("fails the turn before opening Claude for %s", ([launchArgs, expectedMessage]) =>
Effect.scoped(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-append-cwd-" });
const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd);
assert.lengthOf(opened, 0);
assert.isTrue(Exit.isFailure(exit));
if (!Exit.isFailure(exit)) return;
assert.equal(
makeProviderFailure({ cause: exit.cause, class: "provider_error" }).message,
expectedMessage(cwd),
);
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
),
);
});

describe("ClaudeAdapterV2 approval cancellation", () => {
it.effect("observes an approval signal that was already aborted", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -13242,6 +13392,136 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect("keeps the live process when its replacement cannot read the append file", () =>
Effect.scoped(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const cwd = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-claude-append-replacement-",
});
const appendFile = NodePath.join(cwd, "rules.md");
yield* fileSystem.writeFileString(appendFile, "File sentinel\n");
const processQueues: Array<Queue.Queue<SDKMessage>> = [];
let closes = 0;
const firstTurnTerminal = yield* Deferred.make<void>();
const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
settings: {
...DEFAULT_CLAUDE_SETTINGS,
launchArgs: "--append-system-prompt-file rules.md",
},
environment: {},
attachmentsDir: yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-claude-append-replacement-attachments-",
}),
fileSystem,
path: yield* Path.Path,
crypto: yield* Crypto.Crypto,
idAllocator: yield* IdAllocator.IdAllocatorV2,
continuationRequests: { offer: () => Effect.void },
queryRunner: {
allocateSessionId: Effect.succeed(WAKE_NATIVE_SESSION),
open: () =>
Effect.gen(function* () {
const sdkMessages = yield* Queue.unbounded<SDKMessage>();
processQueues.push(sdkMessages);
return {
messages: Stream.fromQueue(sdkMessages),
offer: () => Effect.void,
setModel: () => Effect.void,
applyFlagSettings: () => Effect.void,
setPermissionMode: () => Effect.void,
backgroundTasks: () => Effect.succeed(false),
stopTask: () => Effect.void,
interrupt: Effect.void,
close: Effect.sync(() => {
closes += 1;
}).pipe(Effect.andThen(Queue.shutdown(sdkMessages))),
};
}),
forkSession: () => Effect.die("unused forkSession"),
subagentLaunchToolUseId: () => Effect.succeed(null),
assertComplete: Effect.void,
},
});
const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd,
});
const threadId = ThreadId.make("thread-claude-append-replacement");
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("provider-session-claude-append-replacement"),
modelSelection: CLAUDE_TEST_MODEL_SELECTION,
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection: CLAUDE_TEST_MODEL_SELECTION,
runtimePolicy,
});
yield* runtime.events.pipe(
Stream.runForEach((event) =>
event.type === "turn.terminal"
? Deferred.succeed(firstTurnTerminal, undefined)
: Effect.void,
),
Effect.forkScoped,
);
const now = yield* DateTime.now;

yield* runtime.startTurn(
makeClaudeTestTurnInput({
threadId,
providerThread,
now,
attemptId: RunAttemptId.make("attempt-claude-append-replacement-a"),
text: "What is the sentinel?",
attachments: [],
runtimePolicy,
}),
);
yield* Queue.offer(
processQueues[0]!,
makeResultFrame({
uuid: "00000000-0000-4000-8000-000000000a01",
result: "File sentinel",
}),
);
yield* Deferred.await(firstTurnTerminal);

yield* fileSystem.remove(appendFile);
// A thinking change cannot apply live, so this turn must replace the process.
const refused = yield* runtime
.startTurn(
makeClaudeTestTurnInput({
threadId,
providerThread: { ...providerThread, status: "active" },
now,
attemptId: RunAttemptId.make("attempt-claude-append-replacement-b"),
text: "Switch model.",
attachments: [],
providerTurnOrdinal: 2,
modelSelection: CLAUDE_TEST_RESTART_MODEL_SELECTION,
runtimePolicy,
}),
)
.pipe(Effect.flip);
assert.equal(
makeProviderFailure({ cause: refused, class: "provider_error" }).message,
new ClaudeAdapterV2.ClaudeAppendSystemPromptFileError({ path: appendFile }).message,
);
assert.lengthOf(processQueues, 1);
assert.equal(closes, 0);
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
),
);

it.effect(
"keeps the process and its roster when a model change meets a running background shell",
() =>
Expand Down
Loading
Loading