diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 04ff0a729..9484aac7a 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -2,7 +2,7 @@ # πŸ§ͺ CI Β· Test # ---------------------------------------------------------------------------- # Purpose : Run unit + Playwright e2e tests across Linux & Windows -# Trigger : Push to `main`/`dev`, PRs targeting `main`, manual dispatch +# Trigger : Push to `main`/`dev`, PRs targeting `main` and `dev`, manual dispatch # Jobs : unit β€” `bun turbo test` + config_assistant Go tests on linux # only (windows dropped β€” see # unit-tests matrix comment; free windows-latest runners @@ -10,8 +10,10 @@ # e2e β€” Playwright chromium on linux + windows (matrix) # Gate : Required status check on the `main` ruleset β€” full suite gates # dev β†’ main PRs. Pushes to `dev` also get a full run (dev is the -# integration/testing branch), but feat/fix β†’ dev PRs are gated by -# typecheck only (see ci-typecheck.yml) to keep CI budget sane. +# integration/testing branch). feat/fix β†’ dev PRs run the unit +# matrix as a required check (#370: the Typecheck-only gate let an +# assertion-level regression merge and keep dev red for 75min); +# E2E stays push-on-dev + devβ†’main only to keep CI budget sane. # Notes : `cancel-in-progress: false` β€” every main/dev push gets a full run # No trigger on feat/* or fix/* (frequent changes). # ============================================================================ @@ -26,6 +28,7 @@ on: pull_request: branches: - main + - dev workflow_dispatch: concurrency: diff --git a/.github/workflows/specgit-accept.yml b/.github/workflows/specgit-accept.yml index 2f4792327..6421b235a 100644 --- a/.github/workflows/specgit-accept.yml +++ b/.github/workflows/specgit-accept.yml @@ -13,7 +13,10 @@ jobs: specgit-acceptance: name: SpecGit Acceptance runs-on: ubuntu-latest - timeout-minutes: 15 + # Must exceed the slowest required sibling (Unit Tests (linux) runs + # ~28min on PRs): the verdict waits for every policy check to reach a + # terminal state before evaluating. + timeout-minutes: 45 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -68,7 +71,9 @@ jobs: const retried = [...byName.keys()].find((k) => k.startsWith(name + ' (')); return retried !== undefined && terminal.has(byName.get(retried)); }; - const deadline = Date.now() + 15 * 60 * 1000; + // Must outlast the slowest required sibling (Unit Tests (linux) + // runs ~28min on PRs); the job timeout above bounds this too. + const deadline = Date.now() + 40 * 60 * 1000; while (Date.now() < deadline) { const res = await fetch(url, { headers }); if (!res.ok) throw new Error('check-runs API ' + res.status); diff --git a/.specgit.yaml b/.specgit.yaml index 7a1959b65..3e12d9b8a 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -1,8 +1,8 @@ version: 1 -delivery: issue368 +delivery: issue370 context: kind: branch - branch: feat/368-issue368 + branch: feat/370-issue370 issues: - - 368 -pr: 369 + - 370 +pr: 371 diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts index 7915343ed..04ef202fd 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts @@ -6,6 +6,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" import { InvalidRequestError, ConflictError, notFound } from "../errors" import { Dag } from "@/dag/dag" +import { DagValidation } from "@/dag/validation" import { WorkflowAuthoring } from "@/dag/authoring" import { DagEnvironmentCatalogs } from "@/dag/environment-catalogs" import { createAdmissionRecord } from "@/dag/admission" @@ -176,8 +177,18 @@ export const dagHandlers = HttpApiBuilder.group(InstanceHttpApi, "dag", (handler profile: "environment", environment: { directory: session.directory, parent: session.model ?? undefined }, }) - if (result.prepared?.action !== "start" || result.errors.length > 0) { - const diagnostics = result.errors + // Parity with the workflow tool's start action: model resolution is + // advisory over HTTP β€” the tool asks a question (no model configured + // yet), an API caller has no such interaction; the spawn path fails + // loudly (failWithoutFiber) at execution time if a model never + // resolves. Every other diagnostic class stays blocking, and a graph + // that did not COMPILE (prepared === undefined) is always blocking + // regardless of diagnostic classes. + const blocking = result.errors.filter( + (diagnostic) => diagnostic.code !== DagValidation.DIAGNOSTIC_CODES.modelUnavailable, + ) + if (result.prepared?.action !== "start" || blocking.length > 0) { + const diagnostics = blocking .map((diagnostic) => `- [${diagnostic.code}] ${diagnostic.path}: ${diagnostic.message}${diagnostic.hint ? ` (${diagnostic.hint})` : ""}`) .join("\n") return yield* Effect.fail( diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index 5f06ae976..842399295 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -1913,7 +1913,14 @@ const scenarios: Scenario[] = [ http.protected .post("/dag", "dag.start") .mutating() - .seeded((ctx) => ctx.session({ title: "DAG start owner" })) + .withLlm() + .seeded((ctx) => + // environment-profile authoring resolves each node's model through + // node -> tier -> agent -> parent(session.model); the exerciser's fake + // provider only exists under withLlm, and the parent chain needs the + // session to carry the fake model explicitly. + ctx.session({ title: "DAG start owner", model: { providerID: "test", id: "test-model" } }), + ) .at((ctx) => ({ path: "/dag", headers: ctx.headers(), diff --git a/packages/opencode/test/server/httpapi-exercise/runner.ts b/packages/opencode/test/server/httpapi-exercise/runner.ts index 408360d1f..bc3b7effe 100644 --- a/packages/opencode/test/server/httpapi-exercise/runner.ts +++ b/packages/opencode/test/server/httpapi-exercise/runner.ts @@ -141,7 +141,7 @@ function withContext( return Bun.write(`${directory()}/${name}`, content) }).pipe(Effect.asVoid), session: (input) => - run(modules.Session.Service.use((svc) => svc.create({ title: input?.title, parentID: input?.parentID }))), + run(modules.Session.Service.use((svc) => svc.create({ title: input?.title, parentID: input?.parentID, model: input?.model as never }))), sessionGet: (sessionID) => run(modules.Session.Service.use((svc) => svc.get(sessionID))).pipe( Effect.catchCause(() => Effect.succeed(undefined)), diff --git a/packages/opencode/test/server/httpapi-exercise/types.ts b/packages/opencode/test/server/httpapi-exercise/types.ts index b0dd64777..ee7a4c86b 100644 --- a/packages/opencode/test/server/httpapi-exercise/types.ts +++ b/packages/opencode/test/server/httpapi-exercise/types.ts @@ -54,7 +54,7 @@ export type ScenarioContext = { directory: string | undefined headers: (extra?: Record) => Record file: (name: string, content: string) => Effect.Effect - session: (input?: { title?: string; parentID?: SessionID }) => Effect.Effect + session: (input?: { title?: string; parentID?: SessionID; model?: { id: string; providerID: string } }) => Effect.Effect sessionGet: (sessionID: SessionID) => Effect.Effect project: () => Effect.Effect message: (sessionID: SessionID, input?: { text?: string }) => Effect.Effect diff --git a/spec_git/policy.yaml b/spec_git/policy.yaml index ff8aaa9c9..fe3768c27 100644 --- a/spec_git/policy.yaml +++ b/spec_git/policy.yaml @@ -1,3 +1,4 @@ version: 1 required_checks: - Typecheck + - Unit Tests (linux)