Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
4087 lines
115 KiB
JavaScript
4087 lines
115 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { EventEmitter } from "node:events";
|
|
import { realpathSync } from "node:fs";
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { createServer } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, dirname, join, resolve } from "node:path";
|
|
import test from "node:test";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { createAgentSessionReplayControlWriter } from "../../apps/desktop/src/main/agentSessionReplayStatus.ts";
|
|
import {
|
|
cassettePolicy,
|
|
materializeReplayWorkspaceBlobs,
|
|
parseActivityEvents,
|
|
replayActionFromManifest,
|
|
verifyCassette
|
|
} from "./agent-session-replay-runner/cassette.mjs";
|
|
import {
|
|
enableAgentSessionRecordingFeature,
|
|
replayListenerInfoPath,
|
|
replayWorkbenchSnapshot
|
|
} from "./agent-session-replay-runner/runtime.mjs";
|
|
import {
|
|
resolveAgentSessionReplayProjectRoot,
|
|
resolveRecordScenarioProject,
|
|
seedRecordingUserProject,
|
|
verifyRecordedProjectBindingArtifacts
|
|
} from "./agent-session-replay-runner/recording.mjs";
|
|
import {
|
|
activateRendererReplayWorkspaceCassette,
|
|
assertNoDuplicateEngineSends,
|
|
bootstrapRendererReplayWorkspace,
|
|
bootstrapReplayWorkspace,
|
|
bindManagedReplayShutdown,
|
|
createRendererActivityDriver,
|
|
createReplayActivityClock,
|
|
createReplayPlaybackController,
|
|
createReplayWorkspaceSurfaceReadyQueue,
|
|
createSerialAsyncQueue,
|
|
maybeSettleForScreenshot,
|
|
validateReplayCheckpointPlan,
|
|
assertReplayWorkspaceSucceeded,
|
|
checkpointNeedsToolSettle,
|
|
checkpointNeedsScreenshotSettle,
|
|
checkpointAllowsOptionalScreenshotSettle,
|
|
captureCheckpointScreenshot,
|
|
captureScreenshot,
|
|
hasOpenToolDetailText,
|
|
normalizeScreenshotClip,
|
|
parseArgs,
|
|
resolveDesktopHeadless,
|
|
resolveAgentSessionScreenshotClip,
|
|
replayCheckpointScreenshotPath,
|
|
replayControlRouter,
|
|
replayPendingInteraction,
|
|
resolveReplayProjectFromExpectedState,
|
|
replayObservedTurnId,
|
|
replayStimuli,
|
|
replayStimulusPrecondition,
|
|
replayStimulusRetryableStatus,
|
|
replayStimulusRequest,
|
|
replayTurnIdentityPlan,
|
|
replayWorkspaceTransportRegistrations,
|
|
readReplayTotalDurationMs,
|
|
replayWorkspaceActivityClockOrigin,
|
|
replayWorkspaceInitialTargetCheckpoint,
|
|
managedReplayFailure,
|
|
loadRecordScenario,
|
|
screenshotEvidenceLabel,
|
|
submitRequestedRequiresSessionIdle,
|
|
validateAction,
|
|
validateReplayWorkspaceManifest,
|
|
verifyReplayWorkspaceTransports,
|
|
replayTransportHardFailureMessage
|
|
} from "./run-agent-session-replay.mjs";
|
|
|
|
const replayCassetteAID = "277377ed-af34-454f-a8b9-1047b4064e74";
|
|
const replayCassetteBID = "628c61c4-cbcb-4445-83f7-718bbbd414bd";
|
|
const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
|
|
test("open tool detail accepts short rendered fileChange content", () => {
|
|
assert.equal(hasOpenToolDetailText(["R15_TEST"]), true);
|
|
});
|
|
|
|
test("open tool detail rejects missing or empty content", () => {
|
|
assert.equal(hasOpenToolDetailText([]), false);
|
|
assert.equal(hasOpenToolDetailText([null, undefined, " \n\t "]), false);
|
|
});
|
|
|
|
function respondToCheckpointVerification(request, response) {
|
|
const match = request.url?.match(
|
|
/\/checkpoints\/(?<checkpointIndex>\d+)\/verify$/u
|
|
);
|
|
if (request.method !== "POST" || !match?.groups) return false;
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
canonicalMessageVersion: 1,
|
|
canonicalSessionUpdatedAtUnixMs: 1,
|
|
checkpointIndex: Number(match.groups.checkpointIndex),
|
|
readinessSatisfied: true,
|
|
triggerMatched: true
|
|
})
|
|
);
|
|
return true;
|
|
}
|
|
|
|
test("managed Replay failure preserves a structured startup cause", () => {
|
|
assert.deepEqual(
|
|
managedReplayFailure(
|
|
replayCassetteAID,
|
|
new Error("tuttid exited before publishing listener info", {
|
|
cause: {
|
|
code: "managed_process_stderr",
|
|
message: "unsupported process cassette schema version 2"
|
|
}
|
|
})
|
|
),
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
cause: {
|
|
code: "managed_process_stderr",
|
|
message: "unsupported process cassette schema version 2"
|
|
},
|
|
error: "tuttid exited before publishing listener info"
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Replay Turn identity plan excludes restored historical Turns", () => {
|
|
const session = (turnIds) => ({
|
|
id: "session-1",
|
|
turns: turnIds.map((id) => ({ id }))
|
|
});
|
|
|
|
assert.deepEqual(
|
|
replayTurnIdentityPlan(
|
|
{ agent: { sessions: [session(["turn-old", "turn-new"])] } },
|
|
{ agent: { sessions: [session(["turn-old"])] } }
|
|
),
|
|
{
|
|
"session-1": {
|
|
initialTurnIds: ["turn-old"],
|
|
recordedTurnIds: ["turn-new"]
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Replay Turn identity plan keeps child Session lineage", () => {
|
|
assert.deepEqual(
|
|
replayTurnIdentityPlan({
|
|
agent: {
|
|
sessions: [
|
|
{
|
|
id: "root-recorded",
|
|
kind: "root",
|
|
turns: [{ id: "root-turn-recorded" }]
|
|
},
|
|
{
|
|
id: "child-recorded",
|
|
kind: "child",
|
|
rootSessionId: "root-recorded",
|
|
rootTurnId: "root-turn-recorded",
|
|
parentSessionId: "root-recorded",
|
|
parentTurnId: "root-turn-recorded",
|
|
parentToolCallId: "call-stable",
|
|
turns: [{ id: "child-turn-recorded" }]
|
|
}
|
|
]
|
|
}
|
|
}),
|
|
{
|
|
"root-recorded": {
|
|
initialTurnIds: [],
|
|
recordedTurnIds: ["root-turn-recorded"]
|
|
},
|
|
"child-recorded": {
|
|
initialTurnIds: [],
|
|
recordedTurnIds: ["child-turn-recorded"],
|
|
kind: "child",
|
|
initialSession: false,
|
|
rootSessionId: "root-recorded",
|
|
rootTurnId: "root-turn-recorded",
|
|
parentSessionId: "root-recorded",
|
|
parentTurnId: "root-turn-recorded",
|
|
parentToolCallId: "call-stable"
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
test("managed Replay stops its Desktop when the owner process exits", async () => {
|
|
const runtime = new EventEmitter();
|
|
runtime.stdout = new EventEmitter();
|
|
runtime.stderr = new EventEmitter();
|
|
let checkParent = null;
|
|
let stopCount = 0;
|
|
const dispose = bindManagedReplayShutdown(
|
|
{},
|
|
{
|
|
clearInterval() {},
|
|
isProcessAlive: () => false,
|
|
parentPid: "123",
|
|
processRuntime: runtime,
|
|
setInterval(check) {
|
|
checkParent = check;
|
|
return { unref() {} };
|
|
},
|
|
async stopDesktop() {
|
|
stopCount += 1;
|
|
}
|
|
}
|
|
);
|
|
|
|
checkParent();
|
|
runtime.stdout.emit(
|
|
"error",
|
|
Object.assign(new Error("write EPIPE"), { code: "EPIPE" })
|
|
);
|
|
await Promise.resolve();
|
|
|
|
assert.equal(stopCount, 1);
|
|
dispose();
|
|
assert.equal(runtime.listenerCount("SIGINT"), 0);
|
|
assert.equal(runtime.stdout.listenerCount("error"), 0);
|
|
});
|
|
|
|
test("Replay Workspace confirms final Surface readiness serially", async () => {
|
|
const events = [];
|
|
let releaseFirst;
|
|
const firstReady = new Promise((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
const reportReady = createReplayWorkspaceSurfaceReadyQueue(
|
|
async (cassette) => {
|
|
events.push(`start:${cassette.cassetteId}`);
|
|
if (cassette.cassetteId === "cassette-1") {
|
|
await firstReady;
|
|
}
|
|
events.push(`ready:${cassette.cassetteId}`);
|
|
}
|
|
);
|
|
|
|
const first = reportReady({ cassetteId: "cassette-1" });
|
|
const second = reportReady({ cassetteId: "cassette-2" });
|
|
await Promise.resolve();
|
|
|
|
assert.deepEqual(events, ["start:cassette-1"]);
|
|
releaseFirst();
|
|
await Promise.all([first, second]);
|
|
assert.deepEqual(events, [
|
|
"start:cassette-1",
|
|
"ready:cassette-1",
|
|
"start:cassette-2",
|
|
"ready:cassette-2"
|
|
]);
|
|
});
|
|
|
|
test("Replay Workspace continues Surface readiness after one failure", async () => {
|
|
const events = [];
|
|
const reportReady = createReplayWorkspaceSurfaceReadyQueue(
|
|
async (cassette) => {
|
|
events.push(cassette.cassetteId);
|
|
if (cassette.cassetteId === "cassette-1") {
|
|
throw new Error("first Surface failed");
|
|
}
|
|
}
|
|
);
|
|
|
|
const first = reportReady({ cassetteId: "cassette-1" });
|
|
const second = reportReady({ cassetteId: "cassette-2" });
|
|
|
|
await assert.rejects(first, /first Surface failed/u);
|
|
await second;
|
|
assert.deepEqual(events, ["cassette-1", "cassette-2"]);
|
|
});
|
|
|
|
test("createSerialAsyncQueue runs Desktop UI tasks one at a time", async () => {
|
|
const events = [];
|
|
let releaseFirst;
|
|
const firstGate = new Promise((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
const enqueue = createSerialAsyncQueue();
|
|
const first = enqueue(async () => {
|
|
events.push("start:a");
|
|
await firstGate;
|
|
events.push("end:a");
|
|
});
|
|
const second = enqueue(async () => {
|
|
events.push("start:b");
|
|
events.push("end:b");
|
|
});
|
|
await Promise.resolve();
|
|
assert.deepEqual(events, ["start:a"]);
|
|
releaseFirst();
|
|
await Promise.all([first, second]);
|
|
assert.deepEqual(events, ["start:a", "end:a", "start:b", "end:b"]);
|
|
});
|
|
|
|
test("maybeSettleForScreenshot pins and clears settle agent session id", async () => {
|
|
const expressions = [];
|
|
const client = {
|
|
async send(method, params) {
|
|
assert.equal(method, "Runtime.evaluate");
|
|
expressions.push(params.expression);
|
|
return { result: { value: true } };
|
|
}
|
|
};
|
|
let settleSawPin = false;
|
|
await maybeSettleForScreenshot(
|
|
{
|
|
async settleForScreenshot() {
|
|
settleSawPin = expressions.some((expression) =>
|
|
expression.includes("__tuttiSettleAgentSessionId = ")
|
|
);
|
|
}
|
|
},
|
|
client,
|
|
1_000,
|
|
{ kind: "tool.completed", tags: ["tool.completed"] },
|
|
"session-r03"
|
|
);
|
|
assert.equal(settleSawPin, true);
|
|
assert.ok(
|
|
expressions.some((expression) =>
|
|
expression.includes('__tuttiSettleAgentSessionId = "session-r03"')
|
|
)
|
|
);
|
|
assert.ok(
|
|
expressions.some((expression) =>
|
|
expression.includes("delete globalThis.__tuttiSettleAgentSessionId")
|
|
)
|
|
);
|
|
});
|
|
|
|
test("working checkpoint settle is scenario opt-in", async () => {
|
|
const client = {
|
|
async send() {
|
|
return { result: { value: true } };
|
|
}
|
|
};
|
|
let settles = 0;
|
|
const scenario = {
|
|
async settleForScreenshot() {
|
|
settles += 1;
|
|
}
|
|
};
|
|
const checkpoint = { kind: "turn.working", tags: ["turn.working"] };
|
|
|
|
await maybeSettleForScreenshot(scenario, client, 1_000, checkpoint);
|
|
scenario.settleForWorkingScreenshot = true;
|
|
await maybeSettleForScreenshot(scenario, client, 1_000, checkpoint);
|
|
|
|
assert.equal(settles, 1);
|
|
});
|
|
|
|
test("replayObservedTurnId prefers Protocol v2 turnId after settle", () => {
|
|
assert.equal(
|
|
replayObservedTurnId({
|
|
activeTurnId: null,
|
|
latestTurn: { turnId: "turn-settled" }
|
|
}),
|
|
"turn-settled"
|
|
);
|
|
assert.equal(
|
|
replayObservedTurnId({
|
|
activeTurnId: "turn-active",
|
|
latestTurn: { turnId: "turn-other" }
|
|
}),
|
|
"turn-active"
|
|
);
|
|
assert.equal(
|
|
replayObservedTurnId({
|
|
activeTurnId: null,
|
|
latestTurn: { id: "turn-legacy" }
|
|
}),
|
|
"turn-legacy"
|
|
);
|
|
assert.equal(
|
|
replayObservedTurnId({
|
|
activeTurnId: null,
|
|
latestTurn: {}
|
|
}),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("replay rebases cancel Turn id from settled latestTurn.turnId", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-cancel-turnid-"));
|
|
const listenerDirectory = join(root, "run");
|
|
await mkdir(listenerDirectory, { recursive: true });
|
|
const verified = [];
|
|
const playbackStartedAt = Date.now();
|
|
const server = createServer((request, response) => {
|
|
response.setHeader("content-type", "application/json");
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/playback`
|
|
) {
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: Date.now() - playbackStartedAt,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
// Settled after early cancel: activeTurnId cleared; Protocol v2 uses turnId.
|
|
response.end(
|
|
JSON.stringify({
|
|
session: {
|
|
activeTurnId: null,
|
|
latestTurn: { turnId: "turn-live-canceled" },
|
|
status: "idle"
|
|
}
|
|
})
|
|
);
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
await writeFile(
|
|
join(listenerDirectory, "tuttid.listener.json"),
|
|
JSON.stringify({
|
|
addr: `127.0.0.1:${address.port}`,
|
|
auth: { token: "test-token" }
|
|
})
|
|
);
|
|
const base = {
|
|
agentSessionId: "session-1",
|
|
workspaceId: "workspace-1"
|
|
};
|
|
await replayStimuli(
|
|
root,
|
|
{
|
|
...base,
|
|
activityEvents: [
|
|
{
|
|
...base,
|
|
kind: "effect",
|
|
occurredAtUnixMs: 1,
|
|
payload: { outcome: "succeeded" },
|
|
sequence: 1,
|
|
type: "session/activate"
|
|
},
|
|
{
|
|
...base,
|
|
kind: "effect",
|
|
occurredAtUnixMs: 2,
|
|
payload: {
|
|
outcome: "succeeded",
|
|
turnId: "turn-recorded-canceled"
|
|
},
|
|
sequence: 2,
|
|
type: "turn/cancel"
|
|
}
|
|
],
|
|
turnIdentityPlan: {
|
|
"session-1": {
|
|
initialTurnIds: [],
|
|
recordedTurnIds: ["turn-recorded-canceled"]
|
|
}
|
|
}
|
|
},
|
|
2_000,
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
rendererDriver: {
|
|
async dispatchIntent() {},
|
|
async verifyEffect(event) {
|
|
verified.push(event);
|
|
}
|
|
},
|
|
checkpoints: [
|
|
{
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] },
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
}
|
|
]
|
|
}
|
|
);
|
|
assert.equal(verified.length, 2);
|
|
assert.equal(verified[1].type, "turn/cancel");
|
|
assert.equal(verified[1].payload.turnId, "turn-live-canceled");
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("replay rebases recorded Turn identities before Engine intents", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-turn-identity-"));
|
|
const listenerDirectory = join(root, "run");
|
|
await mkdir(listenerDirectory, { recursive: true });
|
|
const requests = [];
|
|
const dispatched = [];
|
|
let activeTurnId = null;
|
|
const playbackStartedAt = Date.now();
|
|
const server = createServer((request, response) => {
|
|
requests.push(`${request.method} ${request.url}`);
|
|
response.setHeader("content-type", "application/json");
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/playback`
|
|
) {
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: Date.now() - playbackStartedAt,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
request.method === "POST" &&
|
|
request.url ===
|
|
"/v1/workspaces/workspace-1/agent-sessions/session-1/input"
|
|
) {
|
|
activeTurnId = "turn-replay";
|
|
response.statusCode = 201;
|
|
response.end("{}");
|
|
return;
|
|
}
|
|
response.end(
|
|
JSON.stringify({
|
|
session: {
|
|
activeTurnId,
|
|
latestTurn: { id: "turn-replay" },
|
|
status: activeTurnId ? "working" : "idle"
|
|
}
|
|
})
|
|
);
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
await writeFile(
|
|
join(listenerDirectory, "tuttid.listener.json"),
|
|
JSON.stringify({
|
|
addr: `127.0.0.1:${address.port}`,
|
|
auth: { token: "test-token" }
|
|
})
|
|
);
|
|
const base = {
|
|
agentSessionId: "session-1",
|
|
workspaceId: "workspace-1"
|
|
};
|
|
await replayStimuli(
|
|
root,
|
|
{
|
|
...base,
|
|
activityEvents: [
|
|
{
|
|
...base,
|
|
kind: "direct-stimulus",
|
|
occurredAtUnixMs: 1,
|
|
payload: { content: [{ type: "text", text: "start" }] },
|
|
sequence: 1,
|
|
type: "session.send"
|
|
},
|
|
{
|
|
...base,
|
|
kind: "intent",
|
|
occurredAtUnixMs: 2,
|
|
payload: {
|
|
action: "implement",
|
|
commandId: "plan-command",
|
|
idempotencyKey:
|
|
"plan-implementation:workspace-recorded:session-1:turn-recorded",
|
|
promptKind: "plan-implementation",
|
|
requestId: "turn-recorded",
|
|
turnId: "turn-recorded"
|
|
},
|
|
sequence: 2,
|
|
type: "plan/decisionRequested"
|
|
}
|
|
],
|
|
turnIdentityPlan: {
|
|
"session-1": {
|
|
initialTurnIds: [],
|
|
recordedTurnIds: ["turn-recorded"]
|
|
}
|
|
}
|
|
},
|
|
2_000,
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
rendererDriver: {
|
|
dispatchIntent(event) {
|
|
dispatched.push(event);
|
|
activeTurnId = null;
|
|
},
|
|
async verifyEffect() {}
|
|
},
|
|
checkpoints: [
|
|
{
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] },
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
},
|
|
{
|
|
index: 1,
|
|
kind: "after-activity-event",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 2
|
|
},
|
|
cursor: { activityEventSequence: 2, providerConnections: [] },
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
}
|
|
]
|
|
}
|
|
);
|
|
assert.equal(dispatched[0].payload.turnId, "turn-replay");
|
|
assert.equal(dispatched[0].payload.requestId, "turn-replay");
|
|
assert.equal(
|
|
requests.some((request) => request.includes("turn-recorded")),
|
|
false
|
|
);
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("replay rebases child Session and Turn identities from lineage", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-child-identity-"));
|
|
const listenerDirectory = join(root, "run");
|
|
await mkdir(listenerDirectory, { recursive: true });
|
|
const dispatched = [];
|
|
const verified = [];
|
|
let rootActiveTurnId = "root-turn-live";
|
|
const playbackStartedAt = Date.now();
|
|
const server = createServer((request, response) => {
|
|
response.setHeader("content-type", "application/json");
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/playback`
|
|
) {
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: Date.now() - playbackStartedAt,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
"/v1/workspaces/workspace-1/agent-sessions/root-recorded?projection=messageHydration"
|
|
) {
|
|
response.end(
|
|
JSON.stringify({
|
|
session: {
|
|
id: "root-recorded",
|
|
kind: "root",
|
|
activeTurnId: rootActiveTurnId,
|
|
latestTurn: { id: "root-turn-live" }
|
|
},
|
|
childSessions: [
|
|
{
|
|
id: "child-live",
|
|
kind: "child",
|
|
rootAgentSessionId: "root-recorded",
|
|
rootTurnId: "root-turn-live",
|
|
parentAgentSessionId: "root-recorded",
|
|
parentTurnId: "root-turn-live",
|
|
parentToolCallId: "call-stable",
|
|
activeTurnId: "child-turn-live",
|
|
latestTurn: { id: "child-turn-live" }
|
|
}
|
|
]
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url === "/v1/workspaces/workspace-1/agent-sessions/root-recorded"
|
|
) {
|
|
response.end(
|
|
JSON.stringify({
|
|
session: {
|
|
activeTurnId: rootActiveTurnId,
|
|
latestTurn: { id: "root-turn-live" },
|
|
status: rootActiveTurnId ? "working" : "idle"
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
response.end("{}");
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
await writeFile(
|
|
join(listenerDirectory, "tuttid.listener.json"),
|
|
JSON.stringify({
|
|
addr: `127.0.0.1:${address.port}`,
|
|
auth: { token: "test-token" }
|
|
})
|
|
);
|
|
const rootEvent = {
|
|
agentSessionId: "root-recorded",
|
|
workspaceId: "workspace-1"
|
|
};
|
|
const childEvent = {
|
|
agentSessionId: "child-recorded",
|
|
workspaceId: "workspace-1"
|
|
};
|
|
await replayStimuli(
|
|
root,
|
|
{
|
|
...rootEvent,
|
|
activityEvents: [
|
|
{
|
|
...rootEvent,
|
|
kind: "effect",
|
|
occurredAtUnixMs: 1,
|
|
payload: {},
|
|
sequence: 1,
|
|
type: "session/activate"
|
|
},
|
|
{
|
|
...childEvent,
|
|
kind: "intent",
|
|
occurredAtUnixMs: 2,
|
|
payload: {
|
|
requestId: "request-1",
|
|
turnId: "child-turn-recorded"
|
|
},
|
|
sequence: 2,
|
|
type: "interaction/responseRequested"
|
|
},
|
|
{
|
|
...childEvent,
|
|
correlationId: "recorded-correlation",
|
|
kind: "effect",
|
|
occurredAtUnixMs: 3,
|
|
payload: {
|
|
requestId: "request-1",
|
|
turnId: "child-turn-recorded"
|
|
},
|
|
sequence: 3,
|
|
type: "interaction/respond"
|
|
}
|
|
],
|
|
turnIdentityPlan: {
|
|
"root-recorded": {
|
|
initialTurnIds: [],
|
|
recordedTurnIds: ["root-turn-recorded"]
|
|
},
|
|
"child-recorded": {
|
|
initialTurnIds: [],
|
|
recordedTurnIds: ["child-turn-recorded"],
|
|
kind: "child",
|
|
initialSession: false,
|
|
rootSessionId: "root-recorded",
|
|
rootTurnId: "root-turn-recorded",
|
|
parentSessionId: "root-recorded",
|
|
parentTurnId: "root-turn-recorded",
|
|
parentToolCallId: "call-stable"
|
|
}
|
|
}
|
|
},
|
|
2_000,
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
rendererDriver: {
|
|
async dispatchIntent(event) {
|
|
dispatched.push(event);
|
|
},
|
|
async verifyEffect(event) {
|
|
verified.push(event);
|
|
if (event.type === "interaction/respond") {
|
|
rootActiveTurnId = null;
|
|
}
|
|
}
|
|
},
|
|
checkpoints: [
|
|
{
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] },
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
},
|
|
{
|
|
index: 1,
|
|
kind: "after-activity-event",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 3
|
|
},
|
|
cursor: { activityEventSequence: 3, providerConnections: [] },
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
}
|
|
]
|
|
}
|
|
);
|
|
assert.equal(dispatched[0].agentSessionId, "child-live");
|
|
assert.equal(dispatched[0].payload.turnId, "child-turn-live");
|
|
assert.equal(verified[1].agentSessionId, "child-live");
|
|
assert.equal(verified[1].payload.turnId, "child-turn-live");
|
|
assert.match(verified[1].correlationId, /^10:child-live/u);
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("resolves the tuttid listener from the daemon run directory", () => {
|
|
assert.equal(
|
|
replayListenerInfoPath("/tmp/replay-state"),
|
|
"/tmp/replay-state/run/tuttid.listener.json"
|
|
);
|
|
});
|
|
|
|
test("isolated replay workbench suppresses onboarding without preopening AgentGUI", () => {
|
|
const snapshot = replayWorkbenchSnapshot("2026-07-28T00:00:00.000Z");
|
|
assert.deepEqual(snapshot.nodes, []);
|
|
assert.deepEqual(snapshot.nodeStack, []);
|
|
assert.equal(snapshot.activeNodeId, null);
|
|
assert.deepEqual(snapshot.metadata.workspaceOnboarding, {
|
|
autoOpened: true,
|
|
autoOpenedAt: "2026-07-28T00:00:00.000Z",
|
|
schemaVersion: 1
|
|
});
|
|
});
|
|
|
|
test("replay retries only lifecycle readiness conflicts", () => {
|
|
assert.equal(
|
|
replayStimulusPrecondition({
|
|
type: "session.send",
|
|
payload: { guidance: false }
|
|
}),
|
|
"session-idle"
|
|
);
|
|
assert.equal(
|
|
replayStimulusPrecondition({
|
|
type: "session.send",
|
|
payload: { guidance: true }
|
|
}),
|
|
null
|
|
);
|
|
assert.equal(
|
|
replayStimulusPrecondition({ type: "turn.cancel", payload: {} }),
|
|
null
|
|
);
|
|
assert.equal(replayStimulusRetryableStatus("session.create", 502), false);
|
|
assert.equal(replayStimulusRetryableStatus("session.create", 503), false);
|
|
assert.equal(replayStimulusRetryableStatus("session.send", 409), true);
|
|
assert.equal(replayStimulusRetryableStatus("session.send", 502), false);
|
|
assert.equal(
|
|
replayStimulusRetryableStatus("interactive.response", 404),
|
|
false
|
|
);
|
|
assert.deepEqual(
|
|
replayPendingInteraction(
|
|
{
|
|
pendingInteractions: [
|
|
{ requestId: "request-1", status: "pending", turnId: "turn-live" }
|
|
]
|
|
},
|
|
"request-1"
|
|
),
|
|
{ requestId: "request-1", status: "pending", turnId: "turn-live" }
|
|
);
|
|
assert.equal(
|
|
replayPendingInteraction(
|
|
{
|
|
pendingInteractions: [
|
|
{ requestId: "request-1", status: "answered", turnId: "turn-live" }
|
|
]
|
|
},
|
|
"request-1"
|
|
),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("submit idle wait skips busy-queue submits and honors send causation", () => {
|
|
const queuedSubmit = {
|
|
kind: "intent",
|
|
type: "submit/requested",
|
|
eventId: "intent-queued",
|
|
correlationId: "submit-queued",
|
|
payload: {
|
|
routing: "auto",
|
|
submitDiagnostics: { queued: false }
|
|
}
|
|
};
|
|
const drainedSubmit = {
|
|
kind: "intent",
|
|
type: "submit/requested",
|
|
eventId: "intent-drained",
|
|
correlationId: "submit-drained",
|
|
payload: {
|
|
routing: "auto",
|
|
submitDiagnostics: { queued: false }
|
|
}
|
|
};
|
|
const explicitQueued = {
|
|
kind: "intent",
|
|
type: "submit/requested",
|
|
eventId: "intent-explicit",
|
|
payload: {
|
|
routing: "auto",
|
|
submitDiagnostics: { queued: true }
|
|
}
|
|
};
|
|
const sendNow = {
|
|
kind: "intent",
|
|
type: "submit/requested",
|
|
eventId: "intent-send-now",
|
|
payload: {
|
|
routing: "send_now",
|
|
submitDiagnostics: { queued: false }
|
|
}
|
|
};
|
|
const tape = [
|
|
queuedSubmit,
|
|
drainedSubmit,
|
|
{
|
|
kind: "effect",
|
|
type: "queue/sendPrompt",
|
|
eventId: "effect-1",
|
|
causedByEventId: "intent-drained",
|
|
correlationId: "submit-drained",
|
|
payload: { outcome: "succeeded" }
|
|
},
|
|
{
|
|
kind: "intent",
|
|
type: "queue/sendNowRequested",
|
|
eventId: "intent-send-now-req",
|
|
correlationId: "submit-queued"
|
|
},
|
|
{
|
|
kind: "effect",
|
|
type: "queue/sendPrompt",
|
|
eventId: "effect-2",
|
|
causedByEventId: "intent-send-now-req",
|
|
correlationId: "submit-queued",
|
|
payload: { outcome: "succeeded" }
|
|
}
|
|
];
|
|
assert.equal(submitRequestedRequiresSessionIdle(queuedSubmit, tape), false);
|
|
assert.equal(submitRequestedRequiresSessionIdle(drainedSubmit, tape), true);
|
|
assert.equal(submitRequestedRequiresSessionIdle(explicitQueued, tape), false);
|
|
assert.equal(submitRequestedRequiresSessionIdle(sendNow, tape), false);
|
|
});
|
|
|
|
test("replay waits for the previous Turn before each queued send", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-order-"));
|
|
const listenerDirectory = join(root, "run");
|
|
await mkdir(listenerDirectory, { recursive: true });
|
|
let active = false;
|
|
let activeChecks = 0;
|
|
const sent = [];
|
|
const accepted = [];
|
|
const server = createServer((request, response) => {
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/playback`
|
|
) {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: 0,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (request.method === "GET") {
|
|
activeChecks += 1;
|
|
if (activeChecks >= 2) active = false;
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
session: {
|
|
activeTurnId: active ? "turn-active" : null,
|
|
status: active ? "working" : "idle"
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (request.url?.endsWith("/input")) {
|
|
if (active) {
|
|
response.statusCode = 502;
|
|
response.end("agent session already has an active turn");
|
|
return;
|
|
}
|
|
sent.push(request.url);
|
|
active = true;
|
|
activeChecks = 0;
|
|
response.end("{}");
|
|
return;
|
|
}
|
|
active = true;
|
|
activeChecks = 0;
|
|
response.statusCode = 201;
|
|
response.end("{}");
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
await writeFile(
|
|
join(listenerDirectory, "tuttid.listener.json"),
|
|
JSON.stringify({
|
|
addr: `127.0.0.1:${address.port}`,
|
|
auth: { token: "test-token" }
|
|
})
|
|
);
|
|
const base = {
|
|
workspaceId: "workspace-1",
|
|
agentSessionId: "session-1"
|
|
};
|
|
await replayStimuli(
|
|
root,
|
|
{
|
|
...base,
|
|
activityEvents: [
|
|
{
|
|
...base,
|
|
kind: "direct-stimulus",
|
|
sequence: 1,
|
|
occurredAtUnixMs: 1,
|
|
type: "session.send",
|
|
payload: { content: [{ type: "text", text: "second" }] }
|
|
},
|
|
{
|
|
...base,
|
|
kind: "direct-stimulus",
|
|
sequence: 2,
|
|
occurredAtUnixMs: 1,
|
|
type: "session.send",
|
|
payload: { content: [{ type: "text", text: "third" }] }
|
|
}
|
|
]
|
|
},
|
|
2_000,
|
|
{
|
|
checkpoints: [
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] }
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 1,
|
|
kind: "after-activity-event",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 2
|
|
},
|
|
cursor: { activityEventSequence: 2, providerConnections: [] }
|
|
}
|
|
],
|
|
onStimulusAccepted(stimulus) {
|
|
accepted.push(stimulus.type);
|
|
},
|
|
cassetteId: replayCassetteAID
|
|
}
|
|
);
|
|
assert.equal(sent.length, 2);
|
|
assert.deepEqual(accepted, ["session.send", "session.send"]);
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("replay diagnoses a 502 through the exact Cassette transport", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-transport-error-"));
|
|
const listenerDirectory = join(root, "run");
|
|
await mkdir(listenerDirectory, { recursive: true });
|
|
const requests = [];
|
|
const server = createServer((request, response) => {
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
requests.push(`${request.method} ${request.url}`);
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/playback`
|
|
) {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: 0,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
request.method === "POST" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/verify`
|
|
) {
|
|
response.statusCode = 409;
|
|
response.end("recorded outbound frame mismatch");
|
|
return;
|
|
}
|
|
if (request.method === "GET") {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
session: { activeTurnId: null, status: "idle" }
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
response.statusCode = 502;
|
|
response.end("provider transport failed");
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
await writeFile(
|
|
join(listenerDirectory, "tuttid.listener.json"),
|
|
JSON.stringify({
|
|
addr: `127.0.0.1:${address.port}`,
|
|
auth: { token: "test-token" }
|
|
})
|
|
);
|
|
const base = {
|
|
agentSessionId: "session-1",
|
|
workspaceId: "workspace-1"
|
|
};
|
|
await assert.rejects(
|
|
replayStimuli(
|
|
root,
|
|
{
|
|
...base,
|
|
activityEvents: [
|
|
{
|
|
...base,
|
|
kind: "direct-stimulus",
|
|
occurredAtUnixMs: 1,
|
|
payload: { content: [{ type: "text", text: "fail" }] },
|
|
sequence: 1,
|
|
type: "session.send"
|
|
}
|
|
]
|
|
},
|
|
2_000,
|
|
{
|
|
checkpoints: [
|
|
{
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] },
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
}
|
|
],
|
|
cassetteId: replayCassetteAID
|
|
}
|
|
),
|
|
/recorded outbound frame mismatch/u
|
|
);
|
|
assert.equal(
|
|
requests.includes(
|
|
`POST /v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/verify`
|
|
),
|
|
true
|
|
);
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("replay drives intents and verifies effects without sending HTTP", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-activity-"));
|
|
const listenerDirectory = join(root, "run");
|
|
await mkdir(listenerDirectory, { recursive: true });
|
|
const requests = [];
|
|
const playbackStartedAt = Date.now();
|
|
const server = createServer((request, response) => {
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (
|
|
request.method === "GET" &&
|
|
request.url ===
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/transport/playback`
|
|
) {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: Date.now() - playbackStartedAt,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
requests.push(`${request.method} ${request.url}`);
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({ session: { activeTurnId: null, status: "idle" } })
|
|
);
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
await writeFile(
|
|
join(listenerDirectory, "tuttid.listener.json"),
|
|
JSON.stringify({
|
|
addr: `127.0.0.1:${address.port}`,
|
|
auth: { token: "test-token" }
|
|
})
|
|
);
|
|
const base = {
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
scopeId: "workspace-1",
|
|
workspaceId: "workspace-1",
|
|
agentSessionId: "session-1"
|
|
};
|
|
const calls = [];
|
|
const callTimes = [];
|
|
await replayStimuli(
|
|
root,
|
|
{
|
|
workspaceId: base.workspaceId,
|
|
agentSessionId: base.agentSessionId,
|
|
activityEvents: [
|
|
{
|
|
...base,
|
|
sequence: 1,
|
|
occurredAtUnixMs: 1,
|
|
kind: "intent",
|
|
type: "queue/sendNowRequested",
|
|
eventId: "intent-1",
|
|
correlationId: "submit-1",
|
|
payload: { promptId: "prompt-1" }
|
|
},
|
|
{
|
|
...base,
|
|
sequence: 2,
|
|
occurredAtUnixMs: 81,
|
|
kind: "effect",
|
|
type: "session.send",
|
|
eventId: "effect-1",
|
|
correlationId: "submit-1",
|
|
causedByEventId: "intent-1",
|
|
payload: { outcome: "succeeded", guidance: true }
|
|
}
|
|
]
|
|
},
|
|
2_000,
|
|
{
|
|
checkpoints: [
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] }
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 1,
|
|
kind: "after-activity-event",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 2
|
|
},
|
|
cursor: { activityEventSequence: 2, providerConnections: [] }
|
|
}
|
|
],
|
|
rendererDriver: {
|
|
dispatchIntent(event) {
|
|
calls.push(`intent:${event.type}`);
|
|
callTimes.push(Date.now());
|
|
},
|
|
verifyEffect(event) {
|
|
calls.push(`effect:${event.payload.outcome}`);
|
|
callTimes.push(Date.now());
|
|
}
|
|
},
|
|
cassetteId: replayCassetteAID
|
|
}
|
|
);
|
|
assert.deepEqual(calls, [
|
|
"intent:queue/sendNowRequested",
|
|
"effect:succeeded"
|
|
]);
|
|
assert.ok(callTimes[1] - callTimes[0] >= 60);
|
|
assert.equal(
|
|
requests.some((request) => request.startsWith("POST /v1/workspaces/")),
|
|
false
|
|
);
|
|
assert.equal(
|
|
requests.some((request) => request.startsWith("GET /v1/workspaces/")),
|
|
true
|
|
);
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("renderer activity driver scopes bridge calls to one Replay Cassette", async () => {
|
|
const expressions = [];
|
|
const driver = createRendererActivityDriver(
|
|
{
|
|
async send(_method, parameters) {
|
|
expressions.push(parameters.expression);
|
|
return { result: { value: { accepted: true } } };
|
|
}
|
|
},
|
|
2_000,
|
|
replayCassetteAID
|
|
);
|
|
await driver.dispatchIntent({ type: "submit/requested" });
|
|
await driver.verifyEffect({ type: "session.send" });
|
|
assert.equal(expressions.length, 2);
|
|
assert.match(expressions[0], /dispatchCassetteIntent/u);
|
|
assert.match(expressions[0], new RegExp(replayCassetteAID, "u"));
|
|
assert.match(expressions[1], /verifyCassetteEffect/u);
|
|
assert.match(expressions[1], new RegExp(replayCassetteAID, "u"));
|
|
});
|
|
|
|
test("renderer activity driver resumes an exactly-once invocation after CDP collects its Promise", async () => {
|
|
const expressions = [];
|
|
let calls = 0;
|
|
const driver = createRendererActivityDriver(
|
|
{
|
|
async send(_method, parameters) {
|
|
expressions.push(parameters.expression);
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
throw new Error("Promise was collected (-32000)");
|
|
}
|
|
return { result: { value: { accepted: true } } };
|
|
}
|
|
},
|
|
2_000,
|
|
replayCassetteAID
|
|
);
|
|
await driver.dispatchIntent({
|
|
type: "submit/requested",
|
|
eventId: "intent-exactly-once"
|
|
});
|
|
assert.equal(calls, 2);
|
|
assert.equal(expressions[0], expressions[1]);
|
|
assert.match(expressions[0], /__tuttiAgentSessionReplayInvocations/u);
|
|
assert.match(expressions[0], /intent-exactly-once/u);
|
|
});
|
|
|
|
test("renderer activity driver hard-times out when CDP evaluate never resolves", async () => {
|
|
const driver = createRendererActivityDriver(
|
|
{
|
|
async send() {
|
|
return new Promise(() => {
|
|
// Simulate a wedged renderer: CDP never settles.
|
|
});
|
|
}
|
|
},
|
|
50,
|
|
replayCassetteAID
|
|
);
|
|
await assert.rejects(
|
|
() =>
|
|
driver.verifyEffect({
|
|
type: "session/activate",
|
|
eventId: "effect-wedged"
|
|
}),
|
|
/renderer replay invocation timed out after 1s/u
|
|
);
|
|
});
|
|
|
|
test("rejects a direct session.send correlated with a renderer intent", () => {
|
|
assert.throws(
|
|
() =>
|
|
assertNoDuplicateEngineSends([
|
|
{
|
|
kind: "intent",
|
|
type: "submit/requested",
|
|
correlationId: "submit-1"
|
|
},
|
|
{
|
|
kind: "direct-stimulus",
|
|
type: "session.send",
|
|
correlationId: "submit-1"
|
|
}
|
|
]),
|
|
/duplicates renderer intent/u
|
|
);
|
|
});
|
|
|
|
test("parses ordered activity events and requires effects to follow intents", () => {
|
|
const base = {
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
agentSessionId: "session-1",
|
|
occurredAtUnixMs: 1
|
|
};
|
|
const valid = [
|
|
{
|
|
...base,
|
|
sequence: 1,
|
|
kind: "intent",
|
|
type: "submit/requested",
|
|
eventId: "intent-1",
|
|
payload: { content: [] }
|
|
},
|
|
{
|
|
...base,
|
|
sequence: 2,
|
|
kind: "effect",
|
|
type: "session.send",
|
|
eventId: "effect-1",
|
|
causedByEventId: "intent-1",
|
|
payload: { outcome: "succeeded" }
|
|
}
|
|
];
|
|
assert.equal(
|
|
parseActivityEvents(valid.map(JSON.stringify).join("\n")).length,
|
|
2
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseActivityEvents(
|
|
[
|
|
{
|
|
...valid[1],
|
|
sequence: 1
|
|
}
|
|
]
|
|
.map(JSON.stringify)
|
|
.join("\n")
|
|
),
|
|
/earlier intent/u
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseActivityEvents(
|
|
valid
|
|
.map((event, index) =>
|
|
JSON.stringify({
|
|
...event,
|
|
occurredAtUnixMs: index === 0 ? 2 : 1
|
|
})
|
|
)
|
|
.join("\n")
|
|
),
|
|
/is invalid/u
|
|
);
|
|
});
|
|
|
|
test("activity replay clock follows daemon elapsed time and playback state", async () => {
|
|
let playback = {
|
|
paused: false,
|
|
playbackElapsedMs: 0,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
};
|
|
const waits = [];
|
|
let resumeOnNextWait = false;
|
|
const clock = createReplayActivityClock({
|
|
playbackState: async () => playback,
|
|
wait: async (durationMs) => {
|
|
waits.push(durationMs);
|
|
if (!playback.paused && playback.timingMode === "realtime") {
|
|
playback = {
|
|
...playback,
|
|
playbackElapsedMs:
|
|
playback.playbackElapsedMs + durationMs * playback.speed
|
|
};
|
|
}
|
|
if (resumeOnNextWait) {
|
|
resumeOnNextWait = false;
|
|
playback = { ...playback, paused: false };
|
|
}
|
|
}
|
|
});
|
|
|
|
await clock.waitUntil(1_000);
|
|
playback = { ...playback, playbackElapsedMs: 20 };
|
|
await clock.waitUntil(1_100);
|
|
assert.equal(
|
|
waits.reduce((total, duration) => total + duration, 0),
|
|
80
|
|
);
|
|
|
|
playback = { ...playback, speed: 2 };
|
|
const speedWaitStart = waits.length;
|
|
await clock.waitUntil(1_200);
|
|
assert.equal(
|
|
waits
|
|
.slice(speedWaitStart)
|
|
.reduce((total, duration) => total + duration, 0),
|
|
50
|
|
);
|
|
|
|
playback = { ...playback, paused: true };
|
|
const pauseWaitStart = waits.length;
|
|
resumeOnNextWait = true;
|
|
await clock.waitUntil(1_300);
|
|
assert.equal(
|
|
waits
|
|
.slice(pauseWaitStart)
|
|
.reduce((total, duration) => total + duration, 0),
|
|
100
|
|
);
|
|
|
|
playback = { ...playback, timingMode: "fast-forward" };
|
|
const fastForwardWaitStart = waits.length;
|
|
await clock.waitUntil(2_000);
|
|
assert.equal(waits.length, fastForwardWaitStart);
|
|
});
|
|
|
|
test("uses portable checkpoint plan instead of deriving activity boundaries", () => {
|
|
const checkpoints = validateReplayCheckpointPlan(
|
|
{
|
|
schemaVersion: 2,
|
|
cassetteSchemaVersion: cassettePolicy.schemaVersion,
|
|
observationSchemaVersion: 2,
|
|
checkpoints: [
|
|
{
|
|
id: "checkpoint-0000",
|
|
index: 0,
|
|
kind: "replay.bootstrap",
|
|
tags: ["replay.bootstrap"],
|
|
cursor: {
|
|
activityEventSequence: 0,
|
|
providerConnections: []
|
|
},
|
|
trigger: { source: "bootstrap" }
|
|
},
|
|
{
|
|
id: "checkpoint-0001",
|
|
index: 1,
|
|
kind: "submission.accepted",
|
|
tags: ["submission.accepted"],
|
|
cursor: {
|
|
activityEventSequence: 2,
|
|
providerConnections: []
|
|
},
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 2,
|
|
boundaryKind: "intent-effects"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
[
|
|
{ sequence: 1, kind: "intent", eventId: "intent-1" },
|
|
{
|
|
sequence: 2,
|
|
kind: "effect",
|
|
eventId: "effect-1",
|
|
causedByEventId: "intent-1"
|
|
},
|
|
{ sequence: 3, kind: "direct-stimulus", eventId: "send-1" }
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
checkpoints.map((checkpoint) => checkpoint.cursor.activityEventSequence),
|
|
[0, 2]
|
|
);
|
|
});
|
|
|
|
test("rejects checkpoint plan v1 without a compatibility fallback", () => {
|
|
assert.throws(
|
|
() =>
|
|
validateReplayCheckpointPlan(
|
|
{
|
|
schemaVersion: 1,
|
|
cassetteSchemaVersion: cassettePolicy.schemaVersion,
|
|
observationSchemaVersion: 1,
|
|
checkpoints: []
|
|
},
|
|
[]
|
|
),
|
|
/checkpoint_plan_invalid: unsupported plan schema/
|
|
);
|
|
});
|
|
|
|
test("next checkpoint fast-forwards without skipping the stable boundary", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-control-"));
|
|
const controlPath = join(root, "replay-control.json");
|
|
const statusPath = join(root, "replay-status.json");
|
|
const commands = [];
|
|
const requestPaths = [];
|
|
const transportPlayback = {
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: 0,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
};
|
|
const server = createServer(async (request, response) => {
|
|
requestPaths.push(request.url);
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (request.method === "GET") {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(JSON.stringify(transportPlayback));
|
|
return;
|
|
}
|
|
let body = "";
|
|
for await (const chunk of request) body += chunk;
|
|
const command = JSON.parse(body);
|
|
commands.push(command);
|
|
if (command.command === "pause") transportPlayback.paused = true;
|
|
if (command.command === "resume") transportPlayback.paused = false;
|
|
if (command.command === "set-timing-mode") {
|
|
transportPlayback.timingMode = command.timingMode;
|
|
}
|
|
response.setHeader("content-type", "application/json");
|
|
response.end("{}");
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
const checkpoints = [
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] }
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 1,
|
|
kind: "after-activity-event",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 2
|
|
},
|
|
cursor: { activityEventSequence: 2, providerConnections: [] }
|
|
}
|
|
];
|
|
await writeFile(
|
|
controlPath,
|
|
JSON.stringify(replayControlRouter(replayCassetteAID, 0, "resume"))
|
|
);
|
|
const reached = [];
|
|
let replacement = null;
|
|
const playback = createReplayPlaybackController({
|
|
baseURL: `http://127.0.0.1:${address.port}`,
|
|
checkpoints,
|
|
controlPath,
|
|
headers: { "content-type": "application/json" },
|
|
onCheckpoint(checkpoint) {
|
|
reached.push(checkpoint);
|
|
},
|
|
onReplacement(value) {
|
|
replacement = value;
|
|
},
|
|
cassetteId: replayCassetteAID,
|
|
statusPath,
|
|
targetCheckpoint: null,
|
|
timeoutMs: 2_000
|
|
});
|
|
await playback.initialize();
|
|
await writeFile(
|
|
controlPath,
|
|
JSON.stringify(
|
|
replayControlRouter(replayCassetteAID, 1, "next-checkpoint")
|
|
)
|
|
);
|
|
await playback.waitUntilRunnable();
|
|
assert.equal(playback.checkpointAfter(2).index, 1);
|
|
await playback.activityAdvanced(2);
|
|
await playback.reach(checkpoints[1]);
|
|
assert.deepEqual(reached, [1]);
|
|
assert.deepEqual(commands, [
|
|
{ command: "set-provider-cursor", providerConnections: [] },
|
|
{ command: "set-timing-mode", timingMode: "fast-forward" },
|
|
{ command: "resume" },
|
|
{ command: "set-timing-mode", timingMode: "realtime" },
|
|
{ command: "pause" }
|
|
]);
|
|
assert.equal(
|
|
requestPaths.includes(
|
|
`/v1/agent-session-replay/cassettes/${replayCassetteAID}/checkpoints/1/verify`
|
|
),
|
|
true
|
|
);
|
|
assert.deepEqual(JSON.parse(await readFile(statusPath, "utf8")), {
|
|
currentCheckpoint: 1,
|
|
totalCheckpoints: 2,
|
|
paused: true,
|
|
timingMode: "realtime",
|
|
targetCheckpoint: null
|
|
});
|
|
|
|
await writeFile(
|
|
controlPath,
|
|
JSON.stringify(replayControlRouter(replayCassetteAID, 2, "resume"))
|
|
);
|
|
await playback.waitUntilRunnable();
|
|
assert.deepEqual(commands.at(-1), { command: "resume" });
|
|
|
|
let slowOperationSettled = false;
|
|
let settleSlowOperation;
|
|
const slowOperation = new Promise((resolveSlowOperation) => {
|
|
settleSlowOperation = resolveSlowOperation;
|
|
});
|
|
const controlSlowOperation = (async () => {
|
|
await writeFile(
|
|
controlPath,
|
|
JSON.stringify(replayControlRouter(replayCassetteAID, 3, "pause"))
|
|
);
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
if (commands.at(-1)?.command === "pause") break;
|
|
await delay(25);
|
|
}
|
|
assert.equal(slowOperationSettled, false);
|
|
assert.deepEqual(commands.at(-1), { command: "pause" });
|
|
await writeFile(
|
|
controlPath,
|
|
JSON.stringify(replayControlRouter(replayCassetteAID, 4, "resume"))
|
|
);
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
if (commands.at(-1)?.command === "resume") break;
|
|
await delay(25);
|
|
}
|
|
assert.deepEqual(commands.at(-1), { command: "resume" });
|
|
settleSlowOperation();
|
|
})();
|
|
await Promise.all([
|
|
playback
|
|
.runWhilePolling(() => slowOperation)
|
|
.finally(() => {
|
|
slowOperationSettled = true;
|
|
}),
|
|
controlSlowOperation
|
|
]);
|
|
assert.deepEqual(commands.slice(-3), [
|
|
{ command: "pause" },
|
|
{ command: "clear-provider-cursor" },
|
|
{ command: "resume" }
|
|
]);
|
|
|
|
await writeFile(
|
|
controlPath,
|
|
JSON.stringify(
|
|
replayControlRouter(replayCassetteAID, 5, "switch-cassette", {
|
|
cassetteId: "cassette-2"
|
|
})
|
|
)
|
|
);
|
|
await assert.rejects(
|
|
playback.waitForReplacement(() => true),
|
|
/Replay Cassette replacement requested/u
|
|
);
|
|
assert.deepEqual(replacement, {
|
|
command: "switch-cassette",
|
|
currentCheckpoint: 1,
|
|
cassetteId: "cassette-2"
|
|
});
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("manual next holds activity after landing a shared-actSeq checkpoint", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-shared-actseq-"));
|
|
const controlPath = join(root, "replay-control.json");
|
|
const statusPath = join(root, "replay-status.json");
|
|
const commands = [];
|
|
const transportPlayback = {
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: 0,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
};
|
|
const server = createServer(async (request, response) => {
|
|
if (respondToCheckpointVerification(request, response)) return;
|
|
if (request.method === "GET") {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(JSON.stringify(transportPlayback));
|
|
return;
|
|
}
|
|
let body = "";
|
|
for await (const chunk of request) body += chunk;
|
|
const command = JSON.parse(body);
|
|
commands.push(command);
|
|
if (command.command === "pause") transportPlayback.paused = true;
|
|
if (command.command === "resume") transportPlayback.paused = false;
|
|
if (command.command === "set-timing-mode") {
|
|
transportPlayback.timingMode = command.timingMode;
|
|
}
|
|
if (command.command === "set-provider-cursor") {
|
|
transportPlayback.providerConnections = command.providerConnections ?? [];
|
|
}
|
|
if (command.command === "clear-provider-cursor") {
|
|
transportPlayback.providerConnections = [];
|
|
}
|
|
response.setHeader("content-type", "application/json");
|
|
response.end("{}");
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
const providerAt = (chunkSeq) => [
|
|
{ connectionId: "connection-1", chunkSeq, unitIndex: 1 }
|
|
];
|
|
const checkpoints = [
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] }
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 1,
|
|
kind: "tool.started",
|
|
trigger: {
|
|
source: "provider-observation",
|
|
type: "call.started"
|
|
},
|
|
cursor: {
|
|
activityEventSequence: 2,
|
|
providerConnections: providerAt(54)
|
|
}
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 2,
|
|
kind: "interaction.pending",
|
|
trigger: {
|
|
source: "provider-observation",
|
|
type: "interaction.requested"
|
|
},
|
|
cursor: {
|
|
activityEventSequence: 2,
|
|
providerConnections: providerAt(55)
|
|
}
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 3,
|
|
kind: "interaction.resolved",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 3
|
|
},
|
|
cursor: {
|
|
activityEventSequence: 3,
|
|
providerConnections: providerAt(55)
|
|
}
|
|
}
|
|
];
|
|
const reached = [];
|
|
const writeControl = createAgentSessionReplayControlWriter(controlPath);
|
|
const playback = createReplayPlaybackController({
|
|
baseURL: `http://127.0.0.1:${address.port}`,
|
|
checkpoints,
|
|
controlPath,
|
|
headers: { "content-type": "application/json" },
|
|
onCheckpoint(checkpoint) {
|
|
reached.push(checkpoint);
|
|
},
|
|
cassetteId: replayCassetteAID,
|
|
statusPath,
|
|
targetCheckpoint: 1,
|
|
timeoutMs: 2_000
|
|
});
|
|
await playback.initialize();
|
|
await playback.activityAdvanced(2);
|
|
await playback.runWhilePolling(async () => {
|
|
while (!reached.includes(1)) {
|
|
await playback.reach(checkpoints[1]);
|
|
await delay(10);
|
|
}
|
|
});
|
|
assert.deepEqual(reached, [1]);
|
|
|
|
const setProviderCountAfterFirstLand = commands.filter(
|
|
(command) => command.command === "set-provider-cursor"
|
|
).length;
|
|
|
|
await writeControl({
|
|
command: "next-checkpoint",
|
|
cassetteId: replayCassetteAID
|
|
});
|
|
// Duplicate next while seeking must only ack the revision.
|
|
await writeControl({
|
|
command: "next-checkpoint",
|
|
cassetteId: replayCassetteAID
|
|
});
|
|
await playback.runWhilePolling(async () => {
|
|
while (!reached.includes(2)) {
|
|
await playback.reach(checkpoints[2]);
|
|
await delay(10);
|
|
}
|
|
});
|
|
assert.deepEqual(reached, [1, 2]);
|
|
assert.equal(
|
|
commands.filter((command) => command.command === "set-provider-cursor")
|
|
.length,
|
|
setProviderCountAfterFirstLand + 1
|
|
);
|
|
|
|
let activityReleased = false;
|
|
const waiting = playback.waitBeforeActivity(3).then(() => {
|
|
activityReleased = true;
|
|
});
|
|
await playback.runWhilePolling(() => delay(80));
|
|
assert.equal(activityReleased, false);
|
|
|
|
await writeControl({
|
|
command: "next-checkpoint",
|
|
cassetteId: replayCassetteAID
|
|
});
|
|
await playback.runWhilePolling(() => waiting);
|
|
assert.equal(activityReleased, true);
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("Replay Workspace routes Desktop control DTOs to only their Cassette controller", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-control-router-"));
|
|
const controlPath = join(root, "replay-control.json");
|
|
const commands = [];
|
|
const server = createServer(async (request, response) => {
|
|
if (request.method === "GET") {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
drained: false,
|
|
paused: false,
|
|
playbackElapsedMs: 0,
|
|
providerConnections: [],
|
|
speed: 1,
|
|
timingMode: "realtime"
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
let body = "";
|
|
for await (const chunk of request) body += chunk;
|
|
const cassetteId = decodeURIComponent(
|
|
request.url.split("/cassettes/")[1].split("/transport/")[0]
|
|
);
|
|
commands.push({ cassetteId, command: JSON.parse(body) });
|
|
response.setHeader("content-type", "application/json");
|
|
response.end("{}");
|
|
});
|
|
await new Promise((resolveListen) =>
|
|
server.listen(0, "127.0.0.1", resolveListen)
|
|
);
|
|
try {
|
|
const address = server.address();
|
|
assert.notEqual(address, null);
|
|
assert.equal(typeof address, "object");
|
|
const input = {
|
|
baseURL: `http://127.0.0.1:${address.port}`,
|
|
checkpoints: [
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 0,
|
|
kind: "bootstrap",
|
|
trigger: { source: "bootstrap" },
|
|
cursor: { activityEventSequence: 0, providerConnections: [] }
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
index: 1,
|
|
kind: "after-activity-event",
|
|
trigger: {
|
|
source: "activity-boundary",
|
|
afterActivityEventSequence: 1
|
|
},
|
|
cursor: { activityEventSequence: 1, providerConnections: [] }
|
|
}
|
|
],
|
|
controlPath,
|
|
headers: { "content-type": "application/json" },
|
|
targetCheckpoint: null,
|
|
timeoutMs: 2_000
|
|
};
|
|
const controllerA = createReplayPlaybackController({
|
|
...input,
|
|
cassetteId: replayCassetteAID
|
|
});
|
|
const controllerB = createReplayPlaybackController({
|
|
...input,
|
|
cassetteId: replayCassetteBID
|
|
});
|
|
await Promise.all([controllerA.initialize(), controllerB.initialize()]);
|
|
const writeControl = createAgentSessionReplayControlWriter(controlPath);
|
|
|
|
await writeControl({ command: "pause", cassetteId: replayCassetteAID });
|
|
await Promise.all([
|
|
controllerA.runWhilePolling(() => delay(80)),
|
|
controllerB.runWhilePolling(() => delay(80))
|
|
]);
|
|
assert.deepEqual(commands, [
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
command: { command: "pause" }
|
|
}
|
|
]);
|
|
|
|
await writeControl({
|
|
command: "next-checkpoint",
|
|
cassetteId: replayCassetteBID
|
|
});
|
|
await Promise.all([
|
|
controllerA.runWhilePolling(() => delay(80)),
|
|
controllerB.runWhilePolling(() => delay(80))
|
|
]);
|
|
assert.deepEqual(commands.slice(1), [
|
|
{
|
|
cassetteId: replayCassetteBID,
|
|
command: {
|
|
command: "set-provider-cursor",
|
|
providerConnections: []
|
|
}
|
|
},
|
|
{
|
|
cassetteId: replayCassetteBID,
|
|
command: {
|
|
command: "set-timing-mode",
|
|
timingMode: "fast-forward"
|
|
}
|
|
},
|
|
{
|
|
cassetteId: replayCassetteBID,
|
|
command: { command: "resume" }
|
|
}
|
|
]);
|
|
assert.deepEqual(JSON.parse(await readFile(controlPath, "utf8")), {
|
|
schemaVersion: 2,
|
|
cassettes: {
|
|
[replayCassetteAID]: { command: "pause", revision: 1 },
|
|
[replayCassetteBID]: {
|
|
command: "next-checkpoint",
|
|
revision: 1
|
|
}
|
|
}
|
|
});
|
|
} finally {
|
|
await new Promise((resolveClose, rejectClose) =>
|
|
server.close((error) => (error ? rejectClose(error) : resolveClose()))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("record arguments default to headed mode", () => {
|
|
const options = parseArgs([
|
|
"--record",
|
|
".tmp/cassettes/example",
|
|
"--scenario",
|
|
"c01",
|
|
"--scenario-file",
|
|
"/tmp/cases/c01/scenario.mjs",
|
|
"--timeout-ms",
|
|
"1234"
|
|
]);
|
|
assert.equal(options.mode, "record");
|
|
assert.equal(options.headless, undefined);
|
|
assert.equal(resolveDesktopHeadless(options), false);
|
|
assert.equal(options.scenario, "c01");
|
|
assert.equal(options.timeoutMs, 1234);
|
|
assert.equal(options.stallTimeoutMs, 60_000);
|
|
assert.match(
|
|
options.cassetteDirectory,
|
|
/[/\\]\.tmp[/\\]cassettes[/\\]example$/u
|
|
);
|
|
});
|
|
|
|
test("headless flag hides the Electron window", () => {
|
|
const options = parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--headless"
|
|
]);
|
|
assert.equal(options.headless, true);
|
|
assert.equal(resolveDesktopHeadless(options), true);
|
|
});
|
|
|
|
test("managed replay stays headed even with --headless", () => {
|
|
const options = parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--cassette-id",
|
|
"example",
|
|
"--managed",
|
|
"--headless"
|
|
]);
|
|
assert.equal(options.managed, true);
|
|
assert.equal(options.headless, true);
|
|
assert.equal(resolveDesktopHeadless(options), false);
|
|
});
|
|
|
|
test("stall timeout accepts an override and zero disables it", () => {
|
|
const options = parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--stall-timeout-ms",
|
|
"5000"
|
|
]);
|
|
assert.equal(options.stallTimeoutMs, 5000);
|
|
const disabled = parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--stall-timeout-ms",
|
|
"0"
|
|
]);
|
|
assert.equal(disabled.stallTimeoutMs, 0);
|
|
assert.throws(
|
|
() =>
|
|
parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--stall-timeout-ms",
|
|
"-1"
|
|
]),
|
|
/--stall-timeout-ms must be a non-negative integer/
|
|
);
|
|
});
|
|
|
|
test("screenshot checkpoints are opt-in for replay only", () => {
|
|
const options = parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--screenshot-checkpoints"
|
|
]);
|
|
assert.equal(options.screenshotCheckpoints, true);
|
|
const workspace = parseArgs([
|
|
"--replay-workspace-manifest",
|
|
".tmp/replay-workspace.json",
|
|
"--screenshot-checkpoints"
|
|
]);
|
|
assert.equal(workspace.screenshotCheckpoints, true);
|
|
assert.throws(
|
|
() =>
|
|
parseArgs([
|
|
"--record",
|
|
".tmp/cassettes/example",
|
|
"--scenario",
|
|
"c01",
|
|
"--scenario-file",
|
|
"/tmp/cases/c01/scenario.mjs",
|
|
"--screenshot-checkpoints"
|
|
]),
|
|
/--screenshot-checkpoints is only supported with replay/
|
|
);
|
|
});
|
|
|
|
test("checkpoint screenshot paths stay Cassette-scoped in a Replay Workspace", () => {
|
|
const checkpoints = [{ id: "checkpoint-0000" }, { id: "checkpoint-0004" }];
|
|
assert.equal(
|
|
replayCheckpointScreenshotPath({
|
|
artifactDirectory: "/tmp/artifacts",
|
|
checkpointIndex: 4,
|
|
checkpoints: [{ id: "checkpoint-0000" }]
|
|
}),
|
|
join("/tmp/artifacts", "checkpoint-0004.png")
|
|
);
|
|
assert.equal(
|
|
replayCheckpointScreenshotPath({
|
|
artifactDirectory: "/tmp/artifacts",
|
|
cassetteId: "cassette-a",
|
|
checkpointIndex: 1,
|
|
checkpoints
|
|
}),
|
|
join("/tmp/artifacts", "cassette-a", "checkpoint-0004.png")
|
|
);
|
|
});
|
|
|
|
test("normalizeScreenshotClip rejects tiny or invalid rects", () => {
|
|
assert.equal(normalizeScreenshotClip(null), null);
|
|
assert.equal(
|
|
normalizeScreenshotClip({ x: 0, y: 0, width: 4, height: 100 }),
|
|
null
|
|
);
|
|
assert.deepEqual(
|
|
normalizeScreenshotClip({ x: 10.9, y: 20.1, width: 300.7, height: 400.2 }),
|
|
{ x: 10, y: 20, width: 300, height: 400, scale: 1 }
|
|
);
|
|
});
|
|
|
|
test("screenshotEvidenceLabel prefers caseId over scenario", () => {
|
|
assert.equal(screenshotEvidenceLabel(" C03 "), "C03");
|
|
assert.equal(
|
|
screenshotEvidenceLabel({ caseId: "C03", scenario: "c03" }),
|
|
"C03"
|
|
);
|
|
assert.equal(screenshotEvidenceLabel({ scenario: "c03" }), "c03");
|
|
assert.equal(screenshotEvidenceLabel({}), "");
|
|
});
|
|
|
|
test("captureScreenshot clips to the pinned Agent Session and stamps a case badge", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "asr-clip-shot-"));
|
|
const outputPath = join(directory, "checkpoint.png");
|
|
const evaluations = [];
|
|
const captures = [];
|
|
const client = {
|
|
async send(method, params) {
|
|
if (method === "Runtime.evaluate") {
|
|
evaluations.push(params.expression);
|
|
if (params.expression.includes("getBoundingClientRect")) {
|
|
return {
|
|
result: {
|
|
value: { x: 120.4, y: 40.6, width: 640.8, height: 800.2 }
|
|
}
|
|
};
|
|
}
|
|
return { result: { value: true } };
|
|
}
|
|
if (method === "Page.captureScreenshot") {
|
|
captures.push(params);
|
|
// 1x1 PNG
|
|
return {
|
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
|
};
|
|
}
|
|
throw new Error(`unexpected CDP method: ${method}`);
|
|
}
|
|
};
|
|
|
|
const result = await captureScreenshot(client, outputPath, {
|
|
agentSessionId: "session-left",
|
|
label: "C03"
|
|
});
|
|
assert.deepEqual(result.clip, {
|
|
x: 120,
|
|
y: 40,
|
|
width: 640,
|
|
height: 800,
|
|
scale: 1
|
|
});
|
|
assert.equal(captures.length, 1);
|
|
assert.deepEqual(captures[0].clip, result.clip);
|
|
assert.ok(
|
|
evaluations.some((expression) =>
|
|
expression.includes('data-tutti-replay-evidence-badge="true"')
|
|
)
|
|
);
|
|
assert.ok(evaluations.some((expression) => expression.includes('"C03"')));
|
|
assert.ok(
|
|
evaluations.some((expression) =>
|
|
expression.includes(
|
|
"querySelectorAll('[data-tutti-replay-evidence-badge="
|
|
)
|
|
)
|
|
);
|
|
assert.equal((await readFile(outputPath)).length > 0, true);
|
|
});
|
|
|
|
test("captureScreenshot falls back to a full-page shot when the session rect is missing", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "asr-full-shot-"));
|
|
const outputPath = join(directory, "checkpoint.png");
|
|
const captures = [];
|
|
const client = {
|
|
async send(method, params) {
|
|
if (method === "Runtime.evaluate") {
|
|
if (params.expression.includes("getBoundingClientRect")) {
|
|
return { result: { value: null } };
|
|
}
|
|
return { result: { value: true } };
|
|
}
|
|
if (method === "Page.captureScreenshot") {
|
|
captures.push(params);
|
|
return {
|
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
|
};
|
|
}
|
|
throw new Error(`unexpected CDP method: ${method}`);
|
|
}
|
|
};
|
|
|
|
const result = await captureScreenshot(client, outputPath, {
|
|
agentSessionId: "missing-session",
|
|
label: "C05"
|
|
});
|
|
assert.equal(result.clip, null);
|
|
assert.equal(captures.length, 1);
|
|
assert.equal(captures[0].clip, undefined);
|
|
});
|
|
|
|
test("captureCheckpointScreenshot keeps Cassette path and forwards clip identity", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "asr-checkpoint-shot-"));
|
|
const captures = [];
|
|
const client = {
|
|
async send(method, params) {
|
|
if (method === "Runtime.evaluate") {
|
|
if (params.expression.includes("getBoundingClientRect")) {
|
|
return {
|
|
result: { value: { x: 0, y: 0, width: 200, height: 300 } }
|
|
};
|
|
}
|
|
return { result: { value: true } };
|
|
}
|
|
if (method === "Page.captureScreenshot") {
|
|
captures.push(params);
|
|
return {
|
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
|
};
|
|
}
|
|
throw new Error(`unexpected CDP method: ${method}`);
|
|
}
|
|
};
|
|
|
|
const outputPath = await captureCheckpointScreenshot({
|
|
agentSessionId: "session-a",
|
|
artifactDirectory: directory,
|
|
cassetteId: "cassette-a",
|
|
checkpointIndex: 0,
|
|
checkpoints: [{ id: "checkpoint-0007" }],
|
|
client,
|
|
label: "R12"
|
|
});
|
|
assert.equal(
|
|
outputPath,
|
|
join(directory, "cassette-a", "checkpoint-0007.png")
|
|
);
|
|
assert.deepEqual(captures[0]?.clip, {
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 300,
|
|
scale: 1
|
|
});
|
|
});
|
|
|
|
test("resolveAgentSessionScreenshotClip returns null without a session id", async () => {
|
|
assert.equal(
|
|
await resolveAgentSessionScreenshotClip({ send() {} }, ""),
|
|
null
|
|
);
|
|
assert.equal(
|
|
await resolveAgentSessionScreenshotClip({ send() {} }, null),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("record arguments accept an external scenario file", () => {
|
|
const options = parseArgs([
|
|
"--record",
|
|
".tmp/cassettes/example",
|
|
"--scenario",
|
|
"c01",
|
|
"--scenario-file",
|
|
"/tmp/cases/c01/scenario.mjs"
|
|
]);
|
|
assert.equal(options.scenario, "c01");
|
|
assert.equal(options.scenarioFile, "/tmp/cases/c01/scenario.mjs");
|
|
});
|
|
|
|
test("replay arguments accept an optional scenario file for screenshot settle", () => {
|
|
const options = parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/example",
|
|
"--scenario",
|
|
"c01",
|
|
"--scenario-file",
|
|
"/tmp/cases/c01/scenario.mjs",
|
|
"--screenshot-checkpoints"
|
|
]);
|
|
assert.equal(options.mode, "replay");
|
|
assert.equal(options.scenario, "c01");
|
|
assert.equal(options.scenarioFile, "/tmp/cases/c01/scenario.mjs");
|
|
assert.equal(options.screenshotCheckpoints, true);
|
|
});
|
|
|
|
test("checkpoint settle targets completed tool and terminal turn checkpoints", () => {
|
|
assert.equal(checkpointNeedsScreenshotSettle(null), true);
|
|
assert.equal(
|
|
checkpointNeedsScreenshotSettle({
|
|
kind: "tool.completed",
|
|
tags: ["tool.completed"]
|
|
}),
|
|
true
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsScreenshotSettle({
|
|
kind: "turn.terminal",
|
|
tags: ["turn.terminal"]
|
|
}),
|
|
true
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsScreenshotSettle({
|
|
kind: "tool.started",
|
|
tags: ["tool.started"]
|
|
}),
|
|
false
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsScreenshotSettle({
|
|
kind: "submission.accepted",
|
|
tags: ["submission.accepted"]
|
|
}),
|
|
false
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsScreenshotSettle({
|
|
kind: "turn.working",
|
|
tags: ["turn.working"]
|
|
}),
|
|
false
|
|
);
|
|
assert.equal(checkpointNeedsToolSettle(null), false);
|
|
assert.equal(
|
|
checkpointNeedsToolSettle({
|
|
kind: "tool.completed",
|
|
tags: ["tool.completed"]
|
|
}),
|
|
true
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsToolSettle({
|
|
kind: "tool.started",
|
|
tags: ["tool.started"]
|
|
}),
|
|
false
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsToolSettle({
|
|
kind: "turn.terminal",
|
|
tags: ["turn.terminal"]
|
|
}),
|
|
false
|
|
);
|
|
assert.equal(
|
|
checkpointNeedsToolSettle({ kind: "turn.working", tags: ["turn.working"] }),
|
|
false
|
|
);
|
|
assert.equal(
|
|
checkpointAllowsOptionalScreenshotSettle({
|
|
kind: "submission.accepted",
|
|
tags: ["submission.accepted"]
|
|
}),
|
|
true
|
|
);
|
|
assert.equal(
|
|
checkpointAllowsOptionalScreenshotSettle({
|
|
kind: "plan.waiting",
|
|
tags: ["plan.waiting"]
|
|
}),
|
|
true
|
|
);
|
|
assert.equal(
|
|
checkpointAllowsOptionalScreenshotSettle({
|
|
kind: "turn.working",
|
|
tags: ["turn.working"]
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("record scenarios require an id and file and reject raw prompt overrides", () => {
|
|
assert.throws(
|
|
() => parseArgs(["--record", ".tmp/cassettes/example"]),
|
|
/--scenario is required/u
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArgs(["--record", ".tmp/cassettes/example", "--scenario", "c01"]),
|
|
/--scenario and --scenario-file must be provided together/u
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArgs([
|
|
"--record",
|
|
".tmp/cassettes/example",
|
|
"--scenario",
|
|
"i01",
|
|
"--scenario-file",
|
|
"/tmp/cases/i01/scenario.mjs",
|
|
"--prompt",
|
|
"override"
|
|
]),
|
|
/unknown option: --prompt/u
|
|
);
|
|
});
|
|
|
|
test("record runner loads and validates the external scenario module", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "external-scenario-"));
|
|
const scenarioFile = join(directory, "scenario.mjs");
|
|
await writeFile(
|
|
scenarioFile,
|
|
`export default {
|
|
id: "c01",
|
|
prepare() {},
|
|
drive() {},
|
|
assert() {}
|
|
};\n`
|
|
);
|
|
const scenario = await loadRecordScenario({
|
|
scenario: "c01",
|
|
scenarioFile
|
|
});
|
|
assert.equal(scenario.id, "c01");
|
|
await assert.rejects(
|
|
loadRecordScenario({ scenario: "c02", scenarioFile }),
|
|
/does not export scenario c02/u
|
|
);
|
|
});
|
|
|
|
test("record and replay modes are mutually exclusive", () => {
|
|
assert.throws(
|
|
() =>
|
|
parseArgs([
|
|
"--record",
|
|
".tmp/cassettes/record",
|
|
"--replay",
|
|
".tmp/cassettes/replay"
|
|
]),
|
|
/exactly one/u
|
|
);
|
|
});
|
|
|
|
test("managed replay requires the daemon-owned Replay Cassette id", () => {
|
|
assert.throws(
|
|
() => parseArgs(["--replay", ".tmp/cassette", "--managed"]),
|
|
/--cassette-id is required/u
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace arguments accept one fixed batch manifest", () => {
|
|
const options = parseArgs([
|
|
"--replay-workspace-manifest",
|
|
".tmp/replay-workspace.json",
|
|
"--managed"
|
|
]);
|
|
assert.equal(options.mode, "replay-workspace");
|
|
assert.equal(options.managed, true);
|
|
assert.match(
|
|
options.replayWorkspaceManifestPath,
|
|
/[/\\]\.tmp[/\\]replay-workspace\.json$/u
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArgs([
|
|
"--replay-workspace-manifest",
|
|
".tmp/replay-workspace.json",
|
|
"--cassette-id",
|
|
replayCassetteAID
|
|
]),
|
|
/--cassette-id is not supported/u
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace manifest rejects duplicate cassette and root Session", () => {
|
|
const cassette = (cassetteId, rootAgentSessionId) => ({
|
|
cassetteId,
|
|
cassetteDirectory: `.tmp/${cassetteId}`,
|
|
rootAgentSessionId
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
validateReplayWorkspaceManifest({
|
|
playbackMode: "unexpected",
|
|
cassettes: [cassette(replayCassetteAID, "root-a")]
|
|
}),
|
|
/playback mode is invalid/u
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateReplayWorkspaceManifest({
|
|
playbackMode: "automatic",
|
|
workspaceId: "workspace-a",
|
|
cassettes: [
|
|
cassette(replayCassetteAID, "root-a"),
|
|
cassette(replayCassetteAID, "root-b")
|
|
]
|
|
}),
|
|
/duplicate Replay Workspace cassette/u
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateReplayWorkspaceManifest({
|
|
playbackMode: "automatic",
|
|
workspaceId: "workspace-a",
|
|
cassettes: [
|
|
cassette(replayCassetteAID, "root-a"),
|
|
cassette(replayCassetteBID, "root-a")
|
|
]
|
|
}),
|
|
/duplicate Replay Workspace root Session/u
|
|
);
|
|
assert.deepEqual(
|
|
validateReplayWorkspaceManifest({
|
|
playbackMode: "manual",
|
|
cassettes: [{ cassetteDirectory: ".tmp/portable-cassette" }]
|
|
}),
|
|
{
|
|
cassettes: [
|
|
{
|
|
caseId: "",
|
|
cassetteDirectory: join(process.cwd(), ".tmp", "portable-cassette"),
|
|
cassetteId: "",
|
|
rootAgentSessionId: "",
|
|
scenario: "",
|
|
scenarioFile: ""
|
|
}
|
|
],
|
|
playbackMode: "manual",
|
|
workspaceId: null
|
|
}
|
|
);
|
|
assert.equal(
|
|
validateReplayWorkspaceManifest({
|
|
playbackMode: "automatic",
|
|
cassettes: [
|
|
{
|
|
caseId: "C03",
|
|
cassetteDirectory: ".tmp/c03"
|
|
}
|
|
]
|
|
}).cassettes[0]?.caseId,
|
|
"C03"
|
|
);
|
|
});
|
|
|
|
test("manual Replay Workspace starts at its first inspectable checkpoint", () => {
|
|
assert.equal(
|
|
replayWorkspaceInitialTargetCheckpoint(
|
|
{
|
|
action: { type: "continue-session" },
|
|
cassetteId: replayCassetteAID,
|
|
checkpoints: [{}]
|
|
},
|
|
"manual"
|
|
),
|
|
0
|
|
);
|
|
assert.equal(
|
|
replayWorkspaceInitialTargetCheckpoint(
|
|
{
|
|
action: { type: "create-session" },
|
|
cassetteId: replayCassetteBID,
|
|
checkpoints: [{}, {}]
|
|
},
|
|
"manual"
|
|
),
|
|
1
|
|
);
|
|
assert.equal(
|
|
replayWorkspaceInitialTargetCheckpoint(
|
|
{
|
|
action: { type: "create-session" },
|
|
cassetteId: replayCassetteBID,
|
|
checkpoints: [{}]
|
|
},
|
|
"automatic"
|
|
),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace shares the earliest recorded Activity clock origin", () => {
|
|
assert.equal(
|
|
replayWorkspaceActivityClockOrigin([
|
|
{ action: { activityEvents: [{ occurredAtUnixMs: 2_000 }] } },
|
|
{ action: { activityEvents: [{ occurredAtUnixMs: 1_000 }] } }
|
|
]),
|
|
1_000
|
|
);
|
|
assert.equal(
|
|
replayWorkspaceActivityClockOrigin([{ action: { activityEvents: [] } }]),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace rejects duplicate identities derived from artifacts", async () => {
|
|
let runtimeCreates = 0;
|
|
await assert.rejects(
|
|
bootstrapReplayWorkspace(
|
|
{
|
|
playbackMode: "automatic",
|
|
cassettes: [
|
|
{ cassetteDirectory: ".tmp/cassette-a" },
|
|
{ cassetteDirectory: ".tmp/cassette-b" }
|
|
]
|
|
},
|
|
{
|
|
createWorkspaceId: () => "replay-workspace",
|
|
async loadCassette(cassette) {
|
|
return {
|
|
...cassette,
|
|
cassetteId: replayCassetteAID,
|
|
rootAgentSessionId: "root-a"
|
|
};
|
|
},
|
|
async createRuntime() {
|
|
runtimeCreates += 1;
|
|
return {};
|
|
}
|
|
}
|
|
),
|
|
/duplicate Replay Workspace cassette/u
|
|
);
|
|
assert.equal(runtimeCreates, 0);
|
|
});
|
|
|
|
test("Replay Workspace leaves Workspace creation to the semantic runtime", async () => {
|
|
const calls = [];
|
|
const manifest = {
|
|
playbackMode: "automatic",
|
|
workspaceId: "workspace-a",
|
|
cassettes: [
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
cassetteDirectory: ".tmp/cassette-a",
|
|
rootAgentSessionId: "root-a"
|
|
},
|
|
{
|
|
cassetteId: replayCassetteBID,
|
|
cassetteDirectory: ".tmp/cassette-b",
|
|
rootAgentSessionId: "root-b"
|
|
}
|
|
]
|
|
};
|
|
const bootstrap = await bootstrapReplayWorkspace(manifest, {
|
|
async loadCassette(cassette) {
|
|
calls.push(`load:${cassette.cassetteId}`);
|
|
return {
|
|
...cassette,
|
|
action: { activityEvents: [], workspaceId: "workspace-a" },
|
|
mode: "create-session"
|
|
};
|
|
},
|
|
async createRuntime(mode) {
|
|
calls.push(`create:${mode}`);
|
|
return {
|
|
directory: "/runtime",
|
|
stateDirectory: "/runtime/state"
|
|
};
|
|
},
|
|
async initializeDatabase() {
|
|
calls.push("database");
|
|
},
|
|
async materializeBlobs() {
|
|
calls.push("blobs");
|
|
},
|
|
async removeRuntime() {
|
|
calls.push("remove");
|
|
}
|
|
});
|
|
assert.deepEqual(calls, [
|
|
`load:${replayCassetteAID}`,
|
|
`load:${replayCassetteBID}`,
|
|
"create:replay-workspace",
|
|
"database",
|
|
"blobs"
|
|
]);
|
|
assert.equal(bootstrap.runtime.directory, "/runtime");
|
|
assert.deepEqual(
|
|
bootstrap.registrations,
|
|
replayWorkspaceTransportRegistrations(bootstrap.cassettes)
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace seeds each portable project once before blobs", async () => {
|
|
const calls = [];
|
|
const sharedProject = resolveRecordScenarioProject(
|
|
{ label: "repo", relativePath: "." },
|
|
workspaceRoot
|
|
);
|
|
const nestedProject = resolveRecordScenarioProject(
|
|
{ label: "agent", relativePath: "packages/agent" },
|
|
workspaceRoot
|
|
);
|
|
await bootstrapReplayWorkspace(
|
|
{
|
|
playbackMode: "automatic",
|
|
workspaceId: "workspace-a",
|
|
cassettes: [
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
cassetteDirectory: ".tmp/cassette-a",
|
|
rootAgentSessionId: "root-a"
|
|
},
|
|
{
|
|
cassetteId: replayCassetteBID,
|
|
cassetteDirectory: ".tmp/cassette-b",
|
|
rootAgentSessionId: "root-b"
|
|
},
|
|
{
|
|
cassetteId: "a4509255-05f8-4420-aa75-d07234cae38e",
|
|
cassetteDirectory: ".tmp/cassette-c",
|
|
rootAgentSessionId: "root-c"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
async loadCassette(cassette) {
|
|
return {
|
|
...cassette,
|
|
action: {
|
|
replayProject:
|
|
cassette.rootAgentSessionId === "root-c"
|
|
? nestedProject
|
|
: sharedProject,
|
|
workspaceId: "workspace-a"
|
|
},
|
|
mode: "create-session"
|
|
};
|
|
},
|
|
async createRuntime() {
|
|
return {
|
|
directory: "/runtime",
|
|
stateDirectory: "/runtime/state"
|
|
};
|
|
},
|
|
async initializeDatabase(_runtime, workspaceId) {
|
|
calls.push(`database:${workspaceId}`);
|
|
},
|
|
async seedUserProject(databasePath, project) {
|
|
calls.push(`project:${databasePath}:${project.portablePath}`);
|
|
},
|
|
async materializeBlobs() {
|
|
calls.push("blobs");
|
|
}
|
|
}
|
|
);
|
|
assert.deepEqual(calls, [
|
|
"database:workspace-a",
|
|
"project:/runtime/state/tuttid.db:${REPLAY_CWD}",
|
|
"project:/runtime/state/tuttid.db:${REPLAY_CWD}/packages/agent",
|
|
"blobs"
|
|
]);
|
|
});
|
|
|
|
test("Replay registrations carry cassette provider and frozen model metadata", () => {
|
|
assert.deepEqual(
|
|
replayWorkspaceTransportRegistrations([
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
rootAgentSessionId: "root-a",
|
|
cassetteDirectory: "/tmp/cassette-a",
|
|
providers: ["codex"],
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
action: { workspaceId: "workspace-a" }
|
|
}
|
|
]),
|
|
[
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
rootAgentSessionId: "root-a",
|
|
cassetteDirectory: "/tmp/cassette-a/provider",
|
|
artifactDirectory: "/tmp/cassette-a",
|
|
workspaceId: "workspace-a",
|
|
providers: ["codex"],
|
|
frozenModel: "gpt-5.4"
|
|
}
|
|
]
|
|
);
|
|
});
|
|
|
|
test("Replay database enables the agent session recording feature", async () => {
|
|
const databasePath = join(
|
|
tmpdir(),
|
|
`agent-session-replay-feature-${Date.now()}.db`
|
|
);
|
|
try {
|
|
await execFileAsync("sqlite3", [
|
|
databasePath,
|
|
`CREATE TABLE desktop_preferences (
|
|
id TEXT PRIMARY KEY,
|
|
feature_flags_json TEXT
|
|
);
|
|
INSERT INTO desktop_preferences (id, feature_flags_json)
|
|
VALUES ('desktop', '{}');`
|
|
]);
|
|
await enableAgentSessionRecordingFeature(databasePath, workspaceRoot);
|
|
const result = await execFileAsync("sqlite3", [
|
|
databasePath,
|
|
`SELECT json_extract(feature_flags_json, '$."agent.sessionRecording"')
|
|
FROM desktop_preferences WHERE id = 'desktop';`
|
|
]);
|
|
assert.equal(result.stdout.trim(), "1");
|
|
} finally {
|
|
await rm(databasePath, { force: true });
|
|
}
|
|
});
|
|
|
|
test("resolves a project placement from portable expected Session state", () => {
|
|
const project = resolveReplayProjectFromExpectedState(
|
|
{
|
|
agent: {
|
|
sessions: [
|
|
{
|
|
id: "root-a",
|
|
railSectionKey: "project:${REPLAY_CWD}/packages/agent"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"root-a",
|
|
workspaceRoot
|
|
);
|
|
assert.deepEqual(project, {
|
|
id: `replay-project-${createHash("sha256")
|
|
.update(join(workspaceRoot, "packages/agent"))
|
|
.digest("hex")
|
|
.slice(0, 16)}`,
|
|
label: "agent",
|
|
path: join(workspaceRoot, "packages/agent"),
|
|
portablePath: "${REPLAY_CWD}/packages/agent"
|
|
});
|
|
assert.equal(
|
|
resolveReplayProjectFromExpectedState(
|
|
{
|
|
agent: {
|
|
sessions: [{ id: "root-a", railSectionKey: "conversations" }]
|
|
}
|
|
},
|
|
"root-a",
|
|
workspaceRoot
|
|
),
|
|
null
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
resolveReplayProjectFromExpectedState(
|
|
{
|
|
agent: {
|
|
sessions: [
|
|
{ id: "root-a", railSectionKey: "project:/recording/root" }
|
|
]
|
|
}
|
|
},
|
|
"root-a",
|
|
workspaceRoot
|
|
),
|
|
/not portable/u
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace does not create a runtime when any cassette is invalid", async () => {
|
|
let runtimeCreates = 0;
|
|
await assert.rejects(
|
|
bootstrapReplayWorkspace(
|
|
{
|
|
playbackMode: "automatic",
|
|
workspaceId: "workspace-a",
|
|
cassettes: [
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
cassetteDirectory: ".tmp/cassette-a",
|
|
rootAgentSessionId: "root-a"
|
|
},
|
|
{
|
|
cassetteId: replayCassetteBID,
|
|
cassetteDirectory: ".tmp/cassette-b",
|
|
rootAgentSessionId: "root-b"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
async loadCassette(cassette) {
|
|
if (cassette.cassetteId === replayCassetteBID) {
|
|
throw new Error("tampered cassette");
|
|
}
|
|
return cassette;
|
|
},
|
|
async createRuntime() {
|
|
runtimeCreates += 1;
|
|
return {};
|
|
}
|
|
}
|
|
),
|
|
/tampered cassette/u
|
|
);
|
|
assert.equal(runtimeCreates, 0);
|
|
});
|
|
|
|
test("Replay Workspace verifies every Cassette through its cassette-scoped endpoint", async () => {
|
|
const calls = [];
|
|
const results = await verifyReplayWorkspaceTransports(
|
|
"/runtime/state",
|
|
[{ cassetteId: replayCassetteAID }, { cassetteId: replayCassetteBID }],
|
|
1234,
|
|
async (stateDirectory, cassetteId, timeoutMs) => {
|
|
calls.push({ stateDirectory, cassetteId, timeoutMs });
|
|
if (cassetteId === replayCassetteAID) throw new Error("leftover frame");
|
|
}
|
|
);
|
|
assert.deepEqual(calls, [
|
|
{
|
|
stateDirectory: "/runtime/state",
|
|
cassetteId: replayCassetteAID,
|
|
timeoutMs: 1234
|
|
},
|
|
{
|
|
stateDirectory: "/runtime/state",
|
|
cassetteId: replayCassetteBID,
|
|
timeoutMs: 1234
|
|
}
|
|
]);
|
|
assert.deepEqual(results, [
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
verified: false,
|
|
error: "leftover frame"
|
|
},
|
|
{ cassetteId: replayCassetteBID, verified: true }
|
|
]);
|
|
});
|
|
|
|
test("non-managed Replay Workspace propagates Cassette failures", () => {
|
|
assert.throws(
|
|
() =>
|
|
assertReplayWorkspaceSucceeded(
|
|
[
|
|
{ cassetteId: replayCassetteAID, succeeded: true },
|
|
{ cassetteId: replayCassetteBID, succeeded: false }
|
|
],
|
|
false
|
|
),
|
|
new RegExp(replayCassetteBID, "u")
|
|
);
|
|
assert.doesNotThrow(() =>
|
|
assertReplayWorkspaceSucceeded(
|
|
[{ cassetteId: replayCassetteBID, succeeded: false }],
|
|
true
|
|
)
|
|
);
|
|
});
|
|
|
|
test("non-managed Replay Workspace preserves the first Cassette root cause", () => {
|
|
const rootCause = new Error("first cassette failed");
|
|
assert.throws(
|
|
() =>
|
|
assertReplayWorkspaceSucceeded(
|
|
[
|
|
{
|
|
cassetteId: replayCassetteAID,
|
|
error: rootCause,
|
|
succeeded: false
|
|
},
|
|
{ cassetteId: replayCassetteBID, succeeded: false }
|
|
],
|
|
false
|
|
),
|
|
(error) => error === rootCause
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace bootstrap calls the renderer bridge once with all Cassettes", async () => {
|
|
const evaluations = [];
|
|
const snapshot = { ready: false, cassettes: [] };
|
|
const result = await bootstrapRendererReplayWorkspace(
|
|
{
|
|
async send(method, parameters) {
|
|
evaluations.push({ method, parameters });
|
|
return { result: { value: snapshot } };
|
|
}
|
|
},
|
|
[
|
|
{
|
|
action: { agentTargetId: "local:codex" },
|
|
cassetteId: replayCassetteAID,
|
|
rootAgentSessionId: "root-a",
|
|
mode: "create-session",
|
|
ignored: true
|
|
},
|
|
{
|
|
action: { agentTargetId: "local:claude-code" },
|
|
cassetteId: replayCassetteBID,
|
|
rootAgentSessionId: "root-b",
|
|
mode: "continue-session",
|
|
ignored: true
|
|
}
|
|
],
|
|
2_000
|
|
);
|
|
assert.equal(result, snapshot);
|
|
assert.equal(evaluations.length, 1);
|
|
assert.equal(evaluations[0].method, "Runtime.evaluate");
|
|
assert.match(
|
|
evaluations[0].parameters.expression,
|
|
/__tuttiAgentSessionReplayWorkspace/u
|
|
);
|
|
assert.match(
|
|
evaluations[0].parameters.expression,
|
|
new RegExp(replayCassetteAID, "u")
|
|
);
|
|
assert.match(evaluations[0].parameters.expression, /local:codex/u);
|
|
assert.match(
|
|
evaluations[0].parameters.expression,
|
|
new RegExp(replayCassetteBID, "u")
|
|
);
|
|
assert.match(evaluations[0].parameters.expression, /create-session/u);
|
|
assert.match(evaluations[0].parameters.expression, /continue-session/u);
|
|
assert.doesNotMatch(evaluations[0].parameters.expression, /ignored/u);
|
|
});
|
|
|
|
test("Replay Workspace reactivates a created Session before continuing", async () => {
|
|
const evaluations = [];
|
|
await activateRendererReplayWorkspaceCassette(
|
|
{
|
|
async send(method, parameters) {
|
|
evaluations.push({ method, parameters });
|
|
if (evaluations.length === 1) {
|
|
return { result: { value: { ready: false } } };
|
|
}
|
|
return {
|
|
result: {
|
|
value: {
|
|
ready: true,
|
|
cassette: { cassetteId: replayCassetteAID, ready: true }
|
|
}
|
|
}
|
|
};
|
|
}
|
|
},
|
|
replayCassetteAID,
|
|
2_000
|
|
);
|
|
assert.match(evaluations[0].parameters.expression, /\.activate\(/u);
|
|
assert.equal(evaluations.length, 2);
|
|
});
|
|
|
|
test("Replay runner never reloads the page when a Session is created", async () => {
|
|
const source = await readFile(
|
|
new URL("./run-agent-session-replay.mjs", import.meta.url),
|
|
"utf8"
|
|
);
|
|
assert.doesNotMatch(source, /Page\.reload/u);
|
|
});
|
|
|
|
test("managed replacement accepts a stable target checkpoint", () => {
|
|
const options = parseArgs([
|
|
"--replay",
|
|
".tmp/cassette",
|
|
"--managed",
|
|
"--cassette-id",
|
|
"cassette-2",
|
|
"--target-checkpoint",
|
|
"3"
|
|
]);
|
|
assert.equal(options.targetCheckpoint, 3);
|
|
assert.throws(
|
|
() => parseArgs(["--replay", ".tmp/cassette", "--target-checkpoint", "-1"]),
|
|
/non-negative integer/u
|
|
);
|
|
});
|
|
|
|
test("Replay duration includes provider frames and later activity", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-duration-"));
|
|
const providerDirectory = dirname(
|
|
join(root, cassettePolicy.files.providerManifest.path)
|
|
);
|
|
await mkdir(providerDirectory, { recursive: true });
|
|
await writeFile(
|
|
join(providerDirectory, "frames.jsonl"),
|
|
[
|
|
JSON.stringify({ elapsedMs: 1_000 }),
|
|
JSON.stringify({ elapsedMs: 8_000 })
|
|
].join("\n")
|
|
);
|
|
|
|
assert.equal(
|
|
await readReplayTotalDurationMs(root, 10_000, [
|
|
{ occurredAtUnixMs: 11_000 },
|
|
{ occurredAtUnixMs: 19_500 }
|
|
]),
|
|
9_500
|
|
);
|
|
});
|
|
|
|
test("does not accept a source user database", () => {
|
|
assert.throws(
|
|
() =>
|
|
parseArgs([
|
|
"--replay",
|
|
".tmp/cassettes/replay",
|
|
"--source-db",
|
|
"/Users/example/.tutti/tuttid.db"
|
|
]),
|
|
/unknown option/u
|
|
);
|
|
});
|
|
|
|
test("action validation accepts create and continue Session scenarios", () => {
|
|
assert.doesNotThrow(() =>
|
|
validateAction({
|
|
schemaVersion: 1,
|
|
type: "create-session",
|
|
agentTargetId: "local:codex",
|
|
prompts: ["one", "two", "three"]
|
|
})
|
|
);
|
|
assert.doesNotThrow(() =>
|
|
validateAction({
|
|
schemaVersion: 1,
|
|
type: "continue-session",
|
|
agentTargetId: "local:codex",
|
|
prompts: ["next"]
|
|
})
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateAction({
|
|
schemaVersion: 1,
|
|
type: "new-session-submit",
|
|
agentTargetId: "local:codex",
|
|
prompts: ["old"]
|
|
}),
|
|
/invalid or unsupported/u
|
|
);
|
|
});
|
|
|
|
test("materializes verified content-addressed attachment blobs", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-blobs-"));
|
|
const cassette = join(root, "cassette");
|
|
const state = join(root, "state");
|
|
const data = Buffer.from("attachment bytes");
|
|
const digest = createHash("sha256").update(data).digest("hex");
|
|
await mkdir(join(cassette, "blobs", "sha256"), { recursive: true });
|
|
await writeFile(join(cassette, "blobs", "sha256", digest), data);
|
|
await writeFile(
|
|
join(cassette, "blobs", "manifest.json"),
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
blobs: [
|
|
{
|
|
kind: "agent-prompt-attachment",
|
|
sha256: digest,
|
|
sizeBytes: data.byteLength,
|
|
agentSessionId: "session-1",
|
|
attachmentId: "attachment-1",
|
|
mimeType: "image/png"
|
|
}
|
|
]
|
|
})
|
|
);
|
|
await materializeReplayWorkspaceBlobs(
|
|
[{ cassetteDirectory: cassette }],
|
|
state
|
|
);
|
|
assert.deepEqual(
|
|
await readFile(
|
|
join(state, "agent", "attachments", "session-1", "attachment-1.png")
|
|
),
|
|
data
|
|
);
|
|
});
|
|
|
|
test("materializes verified generated image blobs into Codex home", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-generated-image-"));
|
|
const cassette = join(root, "cassette");
|
|
const state = join(root, "state");
|
|
const data = Buffer.from("generated image bytes");
|
|
const digest = createHash("sha256").update(data).digest("hex");
|
|
await mkdir(join(cassette, "blobs", "sha256"), { recursive: true });
|
|
await writeFile(join(cassette, "blobs", "sha256", digest), data);
|
|
await writeFile(
|
|
join(cassette, "blobs", "manifest.json"),
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
blobs: [
|
|
{
|
|
kind: "agent-generated-image",
|
|
sha256: digest,
|
|
sizeBytes: data.byteLength,
|
|
agentSessionId: "session-1",
|
|
relativePath: "generated_images/call-1/image.png",
|
|
mimeType: "image/png"
|
|
}
|
|
]
|
|
})
|
|
);
|
|
await materializeReplayWorkspaceBlobs(
|
|
[{ cassetteDirectory: cassette }],
|
|
state
|
|
);
|
|
assert.deepEqual(
|
|
await readFile(
|
|
join(
|
|
state,
|
|
"agent",
|
|
"runs",
|
|
"session-1",
|
|
"codex-home",
|
|
"generated_images",
|
|
"call-1",
|
|
"image.png"
|
|
)
|
|
),
|
|
data
|
|
);
|
|
});
|
|
|
|
test("Replay Workspace rejects conflicting blobs before materialization", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-blob-conflict-"));
|
|
const state = join(root, "state");
|
|
const cassettes = [];
|
|
for (const [name, contents] of [
|
|
["cassette-a", "one"],
|
|
["cassette-b", "two"]
|
|
]) {
|
|
const cassette = join(root, name);
|
|
const digest = createHash("sha256").update(contents).digest("hex");
|
|
await mkdir(join(cassette, "blobs", "sha256"), { recursive: true });
|
|
await writeFile(join(cassette, "blobs", "sha256", digest), contents);
|
|
await writeFile(
|
|
join(cassette, "blobs", "manifest.json"),
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
blobs: [
|
|
{
|
|
kind: "agent-prompt-attachment",
|
|
sha256: digest,
|
|
sizeBytes: contents.length,
|
|
agentSessionId: "session-a",
|
|
attachmentId: "attachment-a",
|
|
mimeType: "image/png"
|
|
}
|
|
]
|
|
})
|
|
);
|
|
cassettes.push({ cassetteDirectory: cassette });
|
|
}
|
|
await assert.rejects(
|
|
materializeReplayWorkspaceBlobs(cassettes, state),
|
|
/conflicting Replay Workspace blob target/u
|
|
);
|
|
await assert.rejects(
|
|
readFile(
|
|
join(state, "agent", "attachments", "session-a", "attachment-a.png")
|
|
)
|
|
);
|
|
});
|
|
|
|
test("rejects a tampered attachment blob", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-blob-tamper-"));
|
|
const cassette = join(root, "cassette");
|
|
const data = Buffer.from("expected bytes");
|
|
const digest = createHash("sha256").update(data).digest("hex");
|
|
await mkdir(join(cassette, "blobs", "sha256"), { recursive: true });
|
|
await writeFile(
|
|
join(cassette, "blobs", "sha256", digest),
|
|
Buffer.from("tampered")
|
|
);
|
|
await writeFile(
|
|
join(cassette, "blobs", "manifest.json"),
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
blobs: [
|
|
{
|
|
kind: "agent-prompt-attachment",
|
|
sha256: digest,
|
|
sizeBytes: data.byteLength,
|
|
agentSessionId: "session-1",
|
|
attachmentId: "attachment-1",
|
|
mimeType: "image/png"
|
|
}
|
|
]
|
|
})
|
|
);
|
|
await assert.rejects(
|
|
materializeReplayWorkspaceBlobs(
|
|
[{ cassetteDirectory: cassette }],
|
|
join(root, "state")
|
|
),
|
|
/integrity mismatch/u
|
|
);
|
|
});
|
|
|
|
test("ignores Finder metadata and rejects other unrelated files", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-inventory-"));
|
|
const cassette = join(root, "cassette");
|
|
await writeValidCassette(cassette);
|
|
await assert.doesNotReject(verifyCassette(cassette));
|
|
await writeFile(join(cassette, ".DS_Store"), "finder metadata");
|
|
await assert.doesNotReject(verifyCassette(cassette));
|
|
await writeFile(join(cassette, "debug.log"), "unrelated");
|
|
await assert.rejects(verifyCassette(cassette), /unrelated file/u);
|
|
});
|
|
|
|
test("rejects a cassette file integrity mismatch", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-integrity-"));
|
|
const cassette = join(root, "cassette");
|
|
await writeValidCassette(cassette);
|
|
await writeFile(
|
|
join(cassette, cassettePolicy.files.expectedState.path),
|
|
"tampered"
|
|
);
|
|
await assert.rejects(verifyCassette(cassette), /integrity mismatch/u);
|
|
});
|
|
|
|
async function writeValidCassette(cassette) {
|
|
const contents = new Map();
|
|
for (const file of Object.values(cassettePolicy.files)) {
|
|
if (!file.required || file.inventory === false) continue;
|
|
contents.set(
|
|
file.path,
|
|
file.path === cassettePolicy.files.blobManifest.path
|
|
? JSON.stringify({
|
|
schemaVersion: cassettePolicy.blobManifestSchemaVersion,
|
|
blobs: []
|
|
})
|
|
: ""
|
|
);
|
|
}
|
|
const files = [];
|
|
let totalBytes = 0;
|
|
for (const [path, content] of contents) {
|
|
const absolute = join(cassette, ...path.split("/"));
|
|
await mkdir(dirname(absolute), { recursive: true });
|
|
await writeFile(absolute, content);
|
|
const bytes = Buffer.from(content);
|
|
const policyFile = Object.values(cassettePolicy.files).find(
|
|
(candidate) => candidate.path === path
|
|
);
|
|
files.push({
|
|
path,
|
|
role: policyFile.role,
|
|
sizeBytes: bytes.byteLength,
|
|
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
});
|
|
totalBytes += bytes.byteLength;
|
|
}
|
|
await writeFile(
|
|
join(cassette, cassettePolicy.files.cassetteManifest.path),
|
|
JSON.stringify({
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
stateFormat: "tutti.agent-session-replay-state.v1",
|
|
id: replayCassetteAID,
|
|
name: "Valid Cassette",
|
|
sourceRecordingId: replayCassetteBID,
|
|
agentTargetId: "local:codex",
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
rootAgentSessionId: "root-a",
|
|
mode: "create-session",
|
|
maxTotalBytes: cassettePolicy.limits.maxCassetteBytes,
|
|
createdAtUnixMs: 1,
|
|
totalBytes,
|
|
files
|
|
})
|
|
);
|
|
}
|
|
|
|
test("maps API-origin direct stimuli while Engine events stay renderer-owned", () => {
|
|
const base = {
|
|
workspaceId: "workspace 1",
|
|
agentSessionId: "session/1"
|
|
};
|
|
const cases = [
|
|
[
|
|
{
|
|
...base,
|
|
type: "session.create",
|
|
payload: { agentTargetId: "local:codex", content: [] }
|
|
},
|
|
"/v1/workspaces/workspace%201/agent-sessions",
|
|
"session/1"
|
|
],
|
|
[
|
|
{ ...base, type: "session.send", payload: { guidance: "steer" } },
|
|
"/v1/workspaces/workspace%201/agent-sessions/session%2F1/input",
|
|
undefined
|
|
],
|
|
[
|
|
{ ...base, type: "turn.cancel", payload: { turnId: "turn/1" } },
|
|
"/turns/turn%2F1/cancel",
|
|
undefined
|
|
],
|
|
[
|
|
{
|
|
...base,
|
|
type: "interactive.response",
|
|
payload: { requestId: "request/1", turnId: "turn-1", action: "yes" }
|
|
},
|
|
"/interactives/request%2F1/response",
|
|
"turn-1"
|
|
],
|
|
[
|
|
{
|
|
...base,
|
|
type: "plan.decision",
|
|
payload: {
|
|
turnId: "turn-1",
|
|
requestId: "request-1",
|
|
promptKind: "plan-implementation",
|
|
action: "implement",
|
|
idempotencyKey: "decision-1"
|
|
}
|
|
},
|
|
"/plan-decisions/request-1",
|
|
"plan-implementation"
|
|
],
|
|
[
|
|
{
|
|
...base,
|
|
type: "goal.control",
|
|
payload: {
|
|
action: "set",
|
|
clientSubmitId: "goal-submit-1",
|
|
objective: "ship"
|
|
}
|
|
},
|
|
"/goal",
|
|
"goal-submit-1"
|
|
],
|
|
[
|
|
{
|
|
...base,
|
|
type: "session.settings.update",
|
|
payload: { settings: { planMode: true } }
|
|
},
|
|
"/settings",
|
|
true
|
|
]
|
|
];
|
|
for (const [stimulus, pathSuffix, bodyValue] of cases) {
|
|
const request = replayStimulusRequest(stimulus);
|
|
assert.ok(request.path.includes(pathSuffix), stimulus.type);
|
|
if (bodyValue !== undefined) {
|
|
assert.ok(
|
|
JSON.stringify(request.body).includes(JSON.stringify(bodyValue)),
|
|
stimulus.type
|
|
);
|
|
}
|
|
}
|
|
assert.equal(
|
|
replayStimulusRequest({ ...base, type: "internal.worker" }),
|
|
null
|
|
);
|
|
for (const type of [
|
|
"activation/requested",
|
|
"interaction/responseRequested",
|
|
"plan/decisionRequested",
|
|
"session/cancelRequested",
|
|
"session/settingsUpdateRequested"
|
|
]) {
|
|
assert.equal(replayStimulusRequest({ ...base, type }), null, type);
|
|
}
|
|
});
|
|
|
|
test("injects the transient Replay Workspace at the product event boundary", () => {
|
|
const manifest = {
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
mode: "create-session",
|
|
agentTargetId: "local:codex",
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
rootAgentSessionId: "session-1"
|
|
};
|
|
const sourceWorkspaceId = "11111111-1111-4111-8111-111111111111";
|
|
const replayWorkspaceId = "22222222-2222-4222-8222-222222222222";
|
|
const action = replayActionFromManifest(
|
|
manifest,
|
|
[
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
kind: "direct-stimulus",
|
|
type: "session.create",
|
|
eventId: "create-1",
|
|
agentSessionId: "session-1",
|
|
payload: { displayPrompt: sourceWorkspaceId }
|
|
}
|
|
],
|
|
replayWorkspaceId
|
|
);
|
|
assert.equal(action.workspaceId, replayWorkspaceId);
|
|
assert.equal(action.activityEvents[0].scopeId, replayWorkspaceId);
|
|
assert.equal(action.activityEvents[0].workspaceId, replayWorkspaceId);
|
|
assert.equal(
|
|
action.activityEvents[0].payload.displayPrompt,
|
|
sourceWorkspaceId
|
|
);
|
|
assert.deepEqual(
|
|
action.activityEvents[0].payload.settings,
|
|
manifest.replayPrerequisites.composerDefaults
|
|
);
|
|
});
|
|
|
|
test("resolves portable recording paths for Engine activation", () => {
|
|
const manifest = {
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
mode: "create-session",
|
|
agentTargetId: "local:codex",
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
rootAgentSessionId: "session-1"
|
|
};
|
|
const action = replayActionFromManifest(
|
|
manifest,
|
|
[
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
kind: "intent",
|
|
type: "activation/requested",
|
|
eventId: "create-1",
|
|
agentSessionId: "session-1",
|
|
payload: {
|
|
cwd: "${REPLAY_CWD}",
|
|
displayPrompt: "keep ${REPLAY_CWD} as user text",
|
|
settings: { planMode: true },
|
|
railPlacement: {
|
|
projectPath: "${REPLAY_CWD}/packages/agent",
|
|
sectionKey: "project:${REPLAY_CWD}/packages/agent"
|
|
},
|
|
railSectionKey: "project:${REPLAY_CWD}/packages/agent"
|
|
}
|
|
}
|
|
],
|
|
"22222222-2222-4222-8222-222222222222"
|
|
);
|
|
const payload = action.activityEvents[0].payload;
|
|
const replayProjectRoot = resolveAgentSessionReplayProjectRoot();
|
|
const agentPackagePath = join(replayProjectRoot, "packages", "agent");
|
|
assert.equal(payload.cwd, replayProjectRoot);
|
|
assert.equal(payload.cwd.startsWith(`${workspaceRoot}/`), false);
|
|
assert.notEqual(payload.cwd, workspaceRoot);
|
|
assert.equal(payload.railPlacement.projectPath, agentPackagePath);
|
|
assert.equal(payload.railPlacement.sectionKey.startsWith("project:"), true);
|
|
assert.equal(payload.railPlacement.sectionKey, `project:${agentPackagePath}`);
|
|
assert.equal(
|
|
payload.railPlacement.sectionKey.includes("${REPLAY_CWD}"),
|
|
false
|
|
);
|
|
assert.equal(payload.railSectionKey, payload.railPlacement.sectionKey);
|
|
assert.equal(payload.displayPrompt, "keep ${REPLAY_CWD} as user text");
|
|
assert.deepEqual(payload.settings, {
|
|
...manifest.replayPrerequisites.composerDefaults,
|
|
planMode: true
|
|
});
|
|
});
|
|
|
|
test("materializes portable Composer defaults into creation intent and effect", () => {
|
|
const manifest = {
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
mode: "create-session",
|
|
agentTargetId: "local:codex",
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
rootAgentSessionId: "session-1"
|
|
};
|
|
const action = replayActionFromManifest(
|
|
manifest,
|
|
[
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
kind: "intent",
|
|
type: "activation/requested",
|
|
eventId: "create-1",
|
|
agentSessionId: "session-1",
|
|
payload: { cwd: "", settings: {} }
|
|
},
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 2,
|
|
kind: "effect",
|
|
type: "session/activate",
|
|
eventId: "create-2",
|
|
causedByEventId: "create-1",
|
|
agentSessionId: "session-1",
|
|
payload: { cwd: "", settings: {} }
|
|
}
|
|
],
|
|
"22222222-2222-4222-8222-222222222222"
|
|
);
|
|
for (const event of action.activityEvents) {
|
|
assert.deepEqual(
|
|
event.payload.settings,
|
|
manifest.replayPrerequisites.composerDefaults
|
|
);
|
|
}
|
|
});
|
|
|
|
test("does not apply create-session Composer defaults while continuing a Session", () => {
|
|
const action = replayActionFromManifest(
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
mode: "continue-session",
|
|
agentTargetId: "local:codex",
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
rootAgentSessionId: "session-1"
|
|
},
|
|
[
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
kind: "intent",
|
|
type: "activation/requested",
|
|
eventId: "activate-1",
|
|
agentSessionId: "session-1",
|
|
payload: { cwd: "", settings: {} }
|
|
}
|
|
],
|
|
"22222222-2222-4222-8222-222222222222"
|
|
);
|
|
assert.deepEqual(action.activityEvents[0].payload.settings, {});
|
|
});
|
|
|
|
test("PROJECT_ROOT remaps portable REPLAY_CWD outside Tutti checkout", () => {
|
|
const previous = process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT;
|
|
const externalRoot = resolve(tmpdir(), "tutti.sessionrec-unit-test");
|
|
try {
|
|
delete process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT;
|
|
const defaultRoot = resolveAgentSessionReplayProjectRoot();
|
|
assert.equal(
|
|
defaultRoot.startsWith(join(tmpdir(), "tutti-agent-session-rec")),
|
|
true
|
|
);
|
|
assert.equal(defaultRoot.startsWith(`${workspaceRoot}/`), false);
|
|
assert.notEqual(defaultRoot, workspaceRoot);
|
|
process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT = externalRoot;
|
|
assert.equal(resolveAgentSessionReplayProjectRoot(), externalRoot);
|
|
const project = resolveRecordScenarioProject(
|
|
{ label: "sessionrec", relativePath: "." },
|
|
resolveAgentSessionReplayProjectRoot()
|
|
);
|
|
assert.equal(project.path, externalRoot);
|
|
assert.equal(project.portablePath, "${REPLAY_CWD}");
|
|
assert.equal(project.path.startsWith(workspaceRoot + "/"), false);
|
|
assert.throws(() => {
|
|
process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT = "relative/path";
|
|
resolveAgentSessionReplayProjectRoot();
|
|
}, /must be an absolute path/u);
|
|
process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT = externalRoot;
|
|
const action = replayActionFromManifest(
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
mode: "create-session",
|
|
agentTargetId: "local:codex",
|
|
replayPrerequisites: replayPrerequisitesForTest(),
|
|
rootAgentSessionId: "session-1"
|
|
},
|
|
[
|
|
{
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
kind: "intent",
|
|
type: "activation/requested",
|
|
eventId: "create-1",
|
|
agentSessionId: "session-1",
|
|
payload: {
|
|
cwd: "${REPLAY_CWD}",
|
|
railPlacement: {
|
|
projectPath: "${REPLAY_CWD}",
|
|
sectionKey: "project:${REPLAY_CWD}"
|
|
},
|
|
railSectionKey: "project:${REPLAY_CWD}"
|
|
}
|
|
}
|
|
],
|
|
"22222222-2222-4222-8222-222222222222"
|
|
);
|
|
assert.equal(action.activityEvents[0].payload.cwd, externalRoot);
|
|
assert.equal(
|
|
action.activityEvents[0].payload.railPlacement.projectPath,
|
|
externalRoot
|
|
);
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT;
|
|
} else {
|
|
process.env.TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT = previous;
|
|
}
|
|
}
|
|
});
|
|
|
|
test("P01 selects an in-cwd project and requires portable binding artifacts", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "agent-replay-project-binding-"));
|
|
const canonicalRoot = realpathSync(root);
|
|
const project = resolveRecordScenarioProject(
|
|
{ label: basename(canonicalRoot), relativePath: "." },
|
|
root
|
|
);
|
|
assert.equal(project.path, canonicalRoot);
|
|
assert.equal(project.label, basename(canonicalRoot));
|
|
assert.equal(project.portablePath, "${REPLAY_CWD}");
|
|
assert.throws(
|
|
() =>
|
|
resolveRecordScenarioProject(
|
|
{ label: "Outside", relativePath: ".." },
|
|
root
|
|
),
|
|
/must be inside replay cwd/u
|
|
);
|
|
const databasePath = join(root, "project-seed.db");
|
|
await execFileAsync("sqlite3", [
|
|
databasePath,
|
|
`CREATE TABLE user_projects (
|
|
id TEXT PRIMARY KEY,
|
|
path TEXT NOT NULL UNIQUE,
|
|
label TEXT NOT NULL,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
last_used_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
pinned_at_unix_ms INTEGER NOT NULL DEFAULT 0
|
|
);`
|
|
]);
|
|
await seedRecordingUserProject(databasePath, project);
|
|
const seeded = await execFileAsync("sqlite3", [
|
|
databasePath,
|
|
"SELECT path || '|' || label FROM user_projects;"
|
|
]);
|
|
assert.equal(
|
|
seeded.stdout.trim(),
|
|
`${canonicalRoot}|${basename(canonicalRoot)}`
|
|
);
|
|
|
|
await writeFile(
|
|
join(root, cassettePolicy.files.activityEvents.path),
|
|
`${JSON.stringify({
|
|
agentSessionId: "session-1",
|
|
correlationId: "request-1",
|
|
eventId: "intent-1",
|
|
kind: "intent",
|
|
occurredAtUnixMs: 1,
|
|
payload: {
|
|
cwd: "${REPLAY_CWD}",
|
|
mode: "new",
|
|
railPlacement: {
|
|
kind: "project",
|
|
projectPath: "${REPLAY_CWD}",
|
|
sectionKey: "project:${REPLAY_CWD}"
|
|
},
|
|
requestId: "request-1"
|
|
},
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
type: "activation/requested"
|
|
})}\n${JSON.stringify({
|
|
agentSessionId: "session-1",
|
|
causedByEventId: "intent-1",
|
|
correlationId: "request-1",
|
|
eventId: "effect-1",
|
|
kind: "effect",
|
|
occurredAtUnixMs: 2,
|
|
payload: {
|
|
cwd: "${REPLAY_CWD}",
|
|
mode: "new",
|
|
outcome: "succeeded",
|
|
railPlacement: {
|
|
kind: "project",
|
|
projectPath: "${REPLAY_CWD}",
|
|
sectionKey: "project:${REPLAY_CWD}"
|
|
}
|
|
},
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 2,
|
|
type: "session/activate"
|
|
})}\n`
|
|
);
|
|
await writeFile(
|
|
join(root, cassettePolicy.files.checkpointPlan.path),
|
|
JSON.stringify({
|
|
checkpoints: [
|
|
{
|
|
kind: "project.binding-ready",
|
|
readiness: {
|
|
all: [
|
|
{
|
|
equals: "recorded",
|
|
subject: 0,
|
|
type: "project.binding"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
],
|
|
schemaVersion: cassettePolicy.schemaVersion
|
|
})
|
|
);
|
|
|
|
await verifyRecordedProjectBindingArtifacts(root, "${REPLAY_CWD}");
|
|
});
|
|
|
|
test("P03 accepts a portable project binding restored from initial state", async () => {
|
|
const root = await mkdtemp(
|
|
join(tmpdir(), "agent-replay-continued-project-binding-")
|
|
);
|
|
await writeFile(
|
|
join(root, cassettePolicy.files.activityEvents.path),
|
|
`${JSON.stringify({
|
|
agentSessionId: "session-1",
|
|
correlationId: "request-1",
|
|
eventId: "intent-1",
|
|
kind: "intent",
|
|
occurredAtUnixMs: 1,
|
|
payload: { content: [{ text: "follow up", type: "text" }] },
|
|
schemaVersion: cassettePolicy.schemaVersion,
|
|
sequence: 1,
|
|
type: "submit/requested"
|
|
})}\n`
|
|
);
|
|
await writeFile(
|
|
join(root, cassettePolicy.files.initialState.path),
|
|
JSON.stringify({
|
|
agent: {
|
|
sessions: [
|
|
{
|
|
cwd: "${REPLAY_CWD}",
|
|
id: "session-1",
|
|
railProjectPath: "${REPLAY_CWD}",
|
|
railSectionKey: "project:${REPLAY_CWD}"
|
|
}
|
|
]
|
|
}
|
|
})
|
|
);
|
|
await writeFile(
|
|
join(root, cassettePolicy.files.expectedState.path),
|
|
JSON.stringify({
|
|
agent: {
|
|
sessions: [
|
|
{
|
|
cwd: "${REPLAY_CWD}",
|
|
id: "session-1",
|
|
railProjectPath: "${REPLAY_CWD}",
|
|
railSectionKey: "project:${REPLAY_CWD}"
|
|
}
|
|
]
|
|
}
|
|
})
|
|
);
|
|
await verifyRecordedProjectBindingArtifacts(root, "${REPLAY_CWD}");
|
|
|
|
await writeFile(
|
|
join(root, cassettePolicy.files.expectedState.path),
|
|
JSON.stringify({
|
|
agent: {
|
|
sessions: [
|
|
{
|
|
cwd: "${REPLAY_CWD}",
|
|
id: "session-1",
|
|
railProjectPath: null,
|
|
railSectionKey: "conversations"
|
|
}
|
|
]
|
|
}
|
|
})
|
|
);
|
|
await assert.rejects(
|
|
verifyRecordedProjectBindingArtifacts(root, "${REPLAY_CWD}"),
|
|
/did not preserve portable project binding/u
|
|
);
|
|
});
|
|
|
|
function execFileAsync(command, args) {
|
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
execFile(command, args, (error, stdout, stderr) => {
|
|
if (error) {
|
|
rejectPromise(
|
|
new Error(`${command} failed: ${stderr || error.message}`)
|
|
);
|
|
return;
|
|
}
|
|
resolvePromise({ stderr, stdout });
|
|
});
|
|
});
|
|
}
|
|
|
|
function replayPrerequisitesForTest() {
|
|
return {
|
|
composerDefaults: {
|
|
model: "gpt-5.4",
|
|
permissionModeId: "default",
|
|
reasoningEffort: "medium",
|
|
speed: "normal"
|
|
}
|
|
};
|
|
}
|
|
|
|
test("replayTransportHardFailureMessage classifies hard outbound faults", () => {
|
|
assert.equal(
|
|
replayTransportHardFailureMessage(
|
|
409,
|
|
"connection connection-1 outbound mismatch at chunk 64"
|
|
),
|
|
"connection connection-1 outbound mismatch at chunk 64"
|
|
);
|
|
assert.equal(
|
|
replayTransportHardFailureMessage(
|
|
409,
|
|
"connection connection-1 received unexpected outbound bytes after cassette end"
|
|
),
|
|
"connection connection-1 received unexpected outbound bytes after cassette end"
|
|
);
|
|
assert.equal(
|
|
replayTransportHardFailureMessage(
|
|
409,
|
|
"connection connection-1 consumed 64 of 131 chunks"
|
|
),
|
|
""
|
|
);
|
|
assert.equal(replayTransportHardFailureMessage(500, "outbound mismatch"), "");
|
|
assert.equal(replayTransportHardFailureMessage(409, ""), "");
|
|
});
|