// @effect-diagnostics-next-line nodeBuiltinImport:off - Exercise HTTP authentication with the real SDK.
import * as NodeHttp from "node:http";
import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts";
import { assert, describe, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import type { OpencodeClient, ToolPart } from "@opencode-ai/sdk/v2";
import {
  CheckpointId,
  NodeId,
  ProjectId,
  ProviderInstanceId,
  ProviderSessionId,
  ProviderThreadId,
  ProviderTurnId,
  RunAttemptId,
  RunId,
  MessageId,
  ThreadId,
  type OrchestrationV2ProviderThread,
  type OrchestrationV2ProviderTurn,
} from "@t3tools/contracts";
import { OpenCodeSettings } from "../settings.ts";
import * as Effect from "effect/Effect";
import * as Clock from "effect/Clock";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as Exit from "effect/Exit";
import * as Queue from "effect/Queue";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";

import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
import type * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";

import {
  advanceOpenCodePromptAdmission,
  cancelOpenCodePromptAdmission,
  openCodeBoundaryAfterProviderTurn,
  openCodeChildPermissionRules,
  openCodePermissionRules,
  makeOpenCodeProtocolLogger,
  makeOpenCodeAdapterV2,
  OPENCODE_PROVIDER,
  reconcileOpenCodePromptAdmissionStatus,
} from "./adapter.ts";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";

const encodeUnknownJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
const decodeOpenCodeSettings = Schema.decodeEffect(OpenCodeSettings);
const OPEN_CODE_TEST_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
  serverUrl: "http://test.invalid",
});

function promiseGate<A>() {
  let resolve!: (value: A) => void;
  const promise = new Promise<A>((done) => {
    resolve = done;
  });
  return { promise, resolve };
}

function asyncEventStream() {
  const values: Array<{ value: unknown; handled: ReturnType<typeof promiseGate<void>> }> = [];
  const waiters: Array<(value: IteratorResult<unknown>) => void> = [];
  let previousHandled: ReturnType<typeof promiseGate<void>> | undefined;
  let closed = false;
  return {
    close() {
      closed = true;
      for (const waiter of waiters.splice(0)) waiter({ done: true, value: undefined });
    },
    push(value: unknown) {
      const handled = promiseGate<void>();
      const waiter = waiters.shift();
      if (waiter) {
        previousHandled = handled;
        waiter({ done: false, value });
      } else values.push({ value, handled });
      return handled.promise;
    },
    stream: {
      [Symbol.asyncIterator]() {
        return {
          next: () => {
            previousHandled?.resolve();
            if (closed) return Promise.resolve({ done: true as const, value: undefined });
            const entry = values.shift();
            if (entry !== undefined) {
              previousHandled = entry.handled;
              return Promise.resolve({ done: false as const, value: entry.value });
            }
            return new Promise<IteratorResult<unknown>>((resolve) => waiters.push(resolve));
          },
        };
      },
    },
  };
}

const OPENCODE_TEST_SETTINGS = Schema.decodeSync(OpenCodeSettings)({});

function runtimePolicy(
  runtimeMode: ProviderAdapter.ProviderAdapterV2RuntimePolicy["runtimeMode"],
  override: Partial<ProviderAdapter.ProviderAdapterV2RuntimePolicy> = {},
): ProviderAdapter.ProviderAdapterV2RuntimePolicy {
  return ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
    runtimeMode,
    interactionMode: "default",
    cwd: null,
    ...override,
  });
}

function permissionAction(rules: ReturnType<typeof openCodePermissionRules>, permission: string) {
  return rules.findLast((rule) => rule.permission === "*" || rule.permission === permission)
    ?.action;
}

function providerTurn(input: {
  readonly id: string;
  readonly ordinal: number;
  readonly nativeId: string | null;
}): OrchestrationV2ProviderTurn {
  return {
    id: ProviderTurnId.make(input.id),
    providerThreadId: ProviderThreadId.make("provider-thread:opencode-test"),
    nodeId: NodeId.make(`node:${input.id}`),
    runAttemptId: null,
    nativeTurnRef:
      input.nativeId === null
        ? null
        : { driver: OPENCODE_PROVIDER, nativeId: input.nativeId, strength: "weak" },
    ordinal: input.ordinal,
    status: "completed",
    startedAt: null,
    completedAt: null,
  };
}

const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(function* (
  suffix: string,
  nativeSessionId: string,
  client: object,
  host: Partial<ProviderHost.ProviderHost["Service"]> = {},
) {
  const instanceId = ProviderInstanceId.make(`opencode-${suffix}`);
  const threadId = ThreadId.make(`thread-opencode-${suffix}`);
  const modelSelection = {
    instanceId,
    model: "anthropic/claude-sonnet",
    options: [],
  };
  const policy = runtimePolicy("full-access", { cwd: "/workspace" });
  const adapter = yield* makeOpenCodeAdapterV2({
    instanceId,
    settings: OPEN_CODE_TEST_SETTINGS,
    environment: {},
  }).pipe(
    Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, {
      connectToOpenCodeServer: () => Effect.succeed({ url: "http://test.invalid", external: true }),
      createOpenCodeSdkClient: () => client,
    } as unknown as OpenCodeRuntime.OpenCodeRuntime["Service"]),
    Effect.provideService(ProviderHost.ProviderHost, {
      paths: { cwd: "/workspace" },
      resolveAttachmentPath: () => null,
      ...host,
    } as unknown as ProviderHost.ProviderHost["Service"]),
  );
  const runtime = yield* adapter.openSession({
    threadId,
    providerSessionId: ProviderSessionId.make(`session-opencode-${suffix}`),
    modelSelection,
    runtimePolicy: policy,
  });
  const providerThread = yield* runtime.ensureThread({
    threadId,
    modelSelection,
    runtimePolicy: policy,
  });
  const now = yield* DateTime.now;
  const startTurn = (
    text = "hello",
    startProviderThread: OrchestrationV2ProviderThread = providerThread,
    turnPolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy = policy,
  ) =>
    runtime.startTurn({
      appThread: {
        id: threadId,
        projectId: ProjectId.make(`project-opencode-${suffix}`),
        title: suffix,
        providerInstanceId: modelSelection.instanceId,
        modelSelection,
        runtimeMode: "full-access",
        interactionMode: "default",
        branch: null,
        worktreePath: null,
        activeProviderThreadId: providerThread.id,
        lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
        forkedFrom: null,
        createdBy: "user",
        creationSource: "web",
        createdAt: now,
        updatedAt: now,
        archivedAt: null,
        settledOverride: null,
        settledAt: null,
        lastVisitedAt: null,
        deletedAt: null,
      },
      threadId,
      runId: RunId.make(`run-opencode-${suffix}`),
      runOrdinal: 1,
      providerTurnOrdinal: 1,
      attemptId: RunAttemptId.make(`attempt-opencode-${suffix}`),
      rootNodeId: NodeId.make(`node-opencode-${suffix}`),
      providerThread: startProviderThread,
      message: {
        createdBy: "user",
        creationSource: "web",
        messageId: MessageId.make(`message-opencode-${suffix}`),
        text,
        attachments: [],
      },
      modelSelection,
      runtimePolicy: turnPolicy,
    });
  return {
    nativeSessionId,
    now,
    policy,
    providerThread,
    runId: RunId.make(`run-opencode-${suffix}`),
    runtime,
    startTurn,
    threadId,
  };
});

import * as Tracer from "effect/Tracer";
import { vi } from "vite-plus/test";
const observation = vi.hoisted(() => ({ receipt: undefined as Deferred.Deferred<void> | undefined, onReceiptDone: undefined as (()=>void) | undefined, onGuard: undefined as ((done:boolean,stack:string)=>void) | undefined }));
vi.mock("effect/Deferred", async (importOriginal) => {
  const actual = await importOriginal<typeof import("effect/Deferred")>();
  const fibers = await import("effect/Fiber");
  return {
    ...actual,
    isDoneUnsafe: (deferred: Deferred.Deferred<unknown,unknown>) => {
      const done=actual.isDoneUnsafe(deferred);
      if(deferred===observation.receipt) {
        const stack=new Error().stack??"";
        if(stack.split("\n")[2]?.includes("/server/adapter.ts:")) observation.onGuard?.(done,stack.split("\n").slice(1,4).join("\n"));
      }
      return done;
    },
    makeUnsafe: (...args: Parameters<typeof actual.makeUnsafe>) => {
      const deferred = actual.makeUnsafe(...args);
      if(fibers.getCurrent()?.cache.span?.name === "OpenCode.submitPrompt") {
        observation.receipt = deferred as Deferred.Deferred<void>;
        let stored=deferred.effect;
        Object.defineProperty(deferred,"effect",{configurable:true,get:()=>stored,set:(value)=>{stored=value;if(value)observation.onReceiptDone?.();}});
      }
      return deferred;
    },
  };
});
import { appendFileSync } from "node:fs";
import * as Scheduler from "effect/Scheduler";
import * as Cause from "effect/Cause";
import { execFileSync } from "node:child_process";

const revision = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
const record = (row: string, event: string, data: unknown = {}) => {
  const line=JSON.stringify({ utc: new Date().toISOString(), revision, row, event, data });
  if(process.env.EVIDENCE_HARNESS_LOG) appendFileSync(process.env.EVIDENCE_HARNESS_LOG,line+"\n");
  console.log(line);
};

const evidenceScenario = (row: string, caller: "start" | "steer", boundary: boolean, budget?: number, simultaneous = false, finish = "idle") =>
  Effect.gen(function* () {
    const requestEnded = Deferred.makeUnsafe<void>();
    let callerSettled = false;
    let submitSettled = false;
    observation.receipt = undefined;
    observation.onReceiptDone = undefined;
    observation.onGuard = (done,stack)=>record(row,"actual adapter request-error guard evaluated",{receiptDone:done,callerSettled,submitSettled,stack});
    let rejectCommand!: (error: unknown) => void;
    const rejection = new TypeError("fetch failed", { cause: Object.assign(new Error("Headers Timeout Error"), {name: "HeadersTimeoutError", code: "UND_ERR_HEADERS_TIMEOUT"}) });
    const commandCalled = promiseGate<string>();
    const commandResponse = new Promise<never>((_, reject) => { rejectCommand = reject; });
    const nativeEvents = asyncEventStream();
    const rawStream = nativeEvents.stream;
    let rejectAtNext = false;
    const stream = {
      [Symbol.asyncIterator]() {
        const iterator = rawStream[Symbol.asyncIterator]();
        return {
          next() {
            if (rejectAtNext) {
              rejectAtNext = false;
              record(row, "receipt.handler.completed; reject command synchronously before next native event", {callerSettled});
              rejectCommand(rejection);
            }
            return iterator.next();
          },
        };
      },
    };
    const tracer = Tracer.make({
      span(options) {
        const span = Tracer.nativeTracer.span(options);
        const end = span.end.bind(span);
        span.end = (time, exit) => {
          if (["opencode.session.command", "OpenCode.submitPrompt", "OpencodeAdapter.startTurn", "OpencodeAdapter.steerTurn"].includes(options.name)) {
            record(row, "span.end", { name: options.name, exit: Exit.isSuccess(exit) ? "success" : Cause.pretty(exit.cause), callerSettled, submitSettled, receiptDone: observation.receipt ? Deferred.isDoneUnsafe(observation.receipt) : null });
          }
          if(options.name === "OpenCode.submitPrompt") submitSettled=true;
          end(time, exit);
          if (options.name === "opencode.session.command") Deferred.doneUnsafe(requestEnded, Effect.void);
        };
        return span;
      },
    });
    const program = Effect.gen(function* () {
      record(row, "environment", {kind:"CONTROLLED ADAPTER HARNESS; actual adapter, mocked SDK and native stream; no model or HTTP server",caller,boundary,maxOpsBeforeYield:budget??"default",node:process.version});
      const sessionId = `native-${row}`;
      const harness = yield* makeOpenCodeRuntimeHarness(row, sessionId, {
        event: { subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => {
          options.signal?.addEventListener("abort",()=>nativeEvents.close(),{once:true});
          return { stream };
        } },
        command: { list: async () => ({ data: [{ name: "slow" }] }) },
        session: {
          create: async () => ({data:{id:sessionId,time:{created:1,updated:1}}}),
          get: async () => ({data:{id:sessionId,time:{created:1,updated:1}}}),
          messages: async () => ({data:[]}),
          children: async () => ({data:[]}),
          status: async () => ({data:{[sessionId]:{type:"busy"}}}),
          abort: async () => {record(row,"mock session.abort");return {data:true};},
          promptAsync: async () => {record(row,"mock root promptAsync accepted");return {data:true};},
          command: (input: {messageID:string}) => {record(row,"mock session.command sent",input);submitSettled=false;commandCalled.resolve(input.messageID);return commandResponse;},
        },
      });
      const terminal = yield* Deferred.make<ProviderAdapter.ProviderAdapterV2Event>();
      const events: ProviderAdapter.ProviderAdapterV2Event[] = [];
      yield* harness.runtime.events.pipe(Stream.runForEach(event => Effect.gen(function* () {
        events.push(event);
        if(event.type === "turn.terminal") {record(row,"turn.terminal",event);yield* Deferred.succeed(terminal,event);}
      })),Effect.forkScoped);
      let providerTurnId: ProviderTurnId | undefined;
      if(caller==="steer") {
        yield* harness.startTurn("root prompt remains active");
        const snapshot=yield* harness.runtime.readThreadSnapshot({providerThread:harness.providerThread});
        providerTurnId=snapshot.providerTurns.at(-1)!.id;
        record(row,"active root established",{providerTurnId});
      }
      const submission = caller==="start" ? harness.startTurn("/slow 330") : harness.runtime.steerTurn({
        threadId:harness.threadId,runId:harness.runId,providerThread:harness.providerThread,providerTurnId:providerTurnId!,
        message:{messageId:MessageId.make(`steer-${row}`),text:"/slow 330",attachments:[],createdBy:"user",creationSource:"web"},
      });
      const pending = yield* submission.pipe(Effect.exit,Effect.tap(exit=>Effect.sync(()=>{callerSettled=true;record(row,"caller.returned",{exit:Exit.isSuccess(exit)?"success":Cause.pretty(exit.cause)});})),Effect.forkScoped);
      const messageID=yield* Effect.promise(()=>commandCalled.promise);
      if(boundary && !simultaneous) observation.onReceiptDone=()=>{
        observation.onReceiptDone=undefined;
        record(row,"receipt.done; reject native Promise before receipt resumers",{callerSettled,submitSettled,receiptDone:observation.receipt?Deferred.isDoneUnsafe(observation.receipt):null});
        rejectCommand(rejection);
      };
      record(row,"emit matching user receipt",{messageID});
      yield* Effect.promise(()=>{
        const handled=nativeEvents.push({type:"message.updated",properties:{sessionID:sessionId,info:{id:messageID,sessionID:sessionId,role:"user",time:{created:DateTime.toEpochMillis(harness.now)}}}});
        if(simultaneous){record(row,"reject SDK promise in same JS turn as queued receipt",{callerSettled,submitSettled,receiptDone:observation.receipt?Deferred.isDoneUnsafe(observation.receipt):null});rejectCommand(rejection);}
        return handled;
      });
      if(!boundary) {
        yield* Fiber.join(pending);
        if(finish==="finalize-first") {
          record(row,"emit native idle before HTTP rejection");
          yield* Effect.promise(()=>nativeEvents.push({type:"session.idle",properties:{sessionID:sessionId}}));
          yield* Deferred.await(terminal);
        }
        record(row,"reject command after caller settled",{callerSettled});rejectCommand(rejection);
      }
      yield* Deferred.await(requestEnded);
      yield* Fiber.join(pending);
      yield* Effect.promise(()=>nativeEvents.push({type:"session.status",properties:{sessionID:sessionId,status:{type:"busy"}}}));
      const beforeIdle=yield* harness.runtime.readThreadSnapshot({providerThread:harness.providerThread});
      record(row,"snapshot after request failure before idle",{turns:beforeIdle.providerTurns,terminalCount:events.filter(e=>e.type==="turn.terminal").length});
      if(finish==="eof") {
        record(row,"close native stream at explicit milestone");nativeEvents.close();
      } else if(finish==="interrupt") {
        record(row,"explicit no-terminal milestone before cleanup",{turns:beforeIdle.providerTurns,terminalEvents:events.filter(e=>e.type==="turn.terminal")});
        const turn=beforeIdle.providerTurns.at(-1)!;
        const interrupt=yield* harness.runtime.interruptTurn({providerThread:harness.providerThread,providerTurnId:turn.id}).pipe(Effect.exit);
        record(row,"interrupt call returned",{exit:Exit.isSuccess(interrupt)?"success":Cause.pretty(interrupt.cause)});
        record(row,"emit native idle after interrupt");
        yield* Effect.promise(()=>nativeEvents.push({type:"session.idle",properties:{sessionID:sessionId}}));
      } else if(finish==="provider-error"||finish==="usage-limit") {
        const statusCode=finish==="usage-limit"?429:403;
        record(row,"emit native provider error",{statusCode});
        yield* Effect.promise(()=>nativeEvents.push({type:"session.error",properties:{sessionID:sessionId,error:{name:"APIError",data:{message:"Synthetic provider error after ignored HTTP rejection",statusCode,isRetryable:false}}}}));
      } else if(finish!=="finalize-first") {
        record(row,"emit native idle");
        yield* Effect.promise(()=>nativeEvents.push({type:"session.idle",properties:{sessionID:sessionId}}));
      }
      yield* Deferred.await(terminal);
      const final=yield* harness.runtime.readThreadSnapshot({providerThread:harness.providerThread});
      record(row,"final snapshot",{turns:final.providerTurns,terminals:events.filter(e=>e.type==="turn.terminal")});
      assert.equal(events.filter(e=>e.type==="turn.terminal").length,1);
    }).pipe(Effect.withTracer(tracer));
    return yield* (budget===undefined ? program : program.pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield,budget)));
  }).pipe(Effect.provide(Layer.merge(IdAllocator.layer,McpProviderSessions.layer)),Effect.scoped);

describe("PR 27677 optional controls",()=>{
 it.effect("C1 rejects before matching receipt, start",()=>evidenceScenario("C1-start","start",true,undefined,true));
 it.effect("C1 rejects before matching receipt, steer",()=>evidenceScenario("C1-steer","steer",true,undefined,true));
 it.effect("C4 already finalized idle",()=>evidenceScenario("C4","start",false,undefined,false,"finalize-first"));
 it.effect("C6 provider error",()=>evidenceScenario("C6-provider-error","start",false,undefined,false,"provider-error"));
 it.effect("C6 usage limit",()=>evidenceScenario("C6-usage-limit","start",false,undefined,false,"usage-limit"));
 it.effect("C7 stream EOF",()=>evidenceScenario("C7-EOF","start",false,undefined,false,"eof"));
 it.effect("C10 no terminal until explicit interrupt",()=>evidenceScenario("C10","start",false,undefined,false,"interrupt"));
});
