From 714ea9e22e99ebaefe7dfcd251846925713d6d06 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 29 Aug 2026 19:12:32 +0900 Subject: [PATCH 1/5] test(trackers): cover empty state lookups --- .../src/file-tracker-adapter.test.ts | 11 +++++++++ .../tracker-github/src/tracker-github.test.ts | 24 +++++++++++++++++++ .../tracker-linear/src/tracker-linear.test.ts | 19 +++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/packages/tracker-file/src/file-tracker-adapter.test.ts b/packages/tracker-file/src/file-tracker-adapter.test.ts index 5587707d..e9108ecb 100644 --- a/packages/tracker-file/src/file-tracker-adapter.test.ts +++ b/packages/tracker-file/src/file-tracker-adapter.test.ts @@ -232,6 +232,17 @@ describe("fileTrackerAdapter", () => { }); describe("listIssuesByStates", () => { + it("returns without reading the provider for empty state and ID lookups", async () => { + const project = makeProject(join(testDir, "missing-issues.json")); + + await expect( + fileTrackerAdapter.listIssuesByStates(project, []) + ).resolves.toEqual([]); + await expect( + fileTrackerAdapter.fetchIssueStatesByIds(project, []) + ).resolves.toEqual([]); + }); + it("filters issues to the requested workflow states", async () => { const issuesPath = join(testDir, "issues.json"); await writeFile( diff --git a/packages/tracker-github/src/tracker-github.test.ts b/packages/tracker-github/src/tracker-github.test.ts index 92ba9837..e51b2f64 100644 --- a/packages/tracker-github/src/tracker-github.test.ts +++ b/packages/tracker-github/src/tracker-github.test.ts @@ -2380,6 +2380,30 @@ Prompt`, expect(issues.map((issue) => issue.state)).toEqual(["Done"]); }); + it("does not call GitHub for empty state or ID lookups", async () => { + const adapter = resolveTrackerAdapter({ + adapter: "github-project", + bindingId: "project-123", + settings: { projectId: "project-123" }, + }); + const fetchImpl = vi.fn(); + + await expect( + adapter.listIssuesByStates(makeProjectConfig(), [], { + token: "dependencies-token", + fetchImpl, + }) + ).resolves.toEqual([]); + await expect( + adapter.fetchIssueStatesByIds(makeProjectConfig(), [], { + token: "dependencies-token", + fetchImpl, + }) + ).resolves.toEqual([]); + + expect(fetchImpl).not.toHaveBeenCalled(); + }); + it("keeps the custom lifecycle field when explicit state lookups disable server filtering", async () => { const adapter = resolveTrackerAdapter({ adapter: "github-project", diff --git a/packages/tracker-linear/src/tracker-linear.test.ts b/packages/tracker-linear/src/tracker-linear.test.ts index 3753f64c..d365ce60 100644 --- a/packages/tracker-linear/src/tracker-linear.test.ts +++ b/packages/tracker-linear/src/tracker-linear.test.ts @@ -1150,6 +1150,25 @@ Prompt`, ); }); + it("does not call Linear for empty state or ID lookups", async () => { + const fetchImpl = vi.fn(); + + await expect( + linearTrackerAdapter.listIssuesByStates(makeProject(), [], { + fetchImpl, + token: "linear-token", + }) + ).resolves.toEqual([]); + await expect( + linearTrackerAdapter.fetchIssueStatesByIds(makeProject(), [], { + fetchImpl, + token: "linear-token", + }) + ).resolves.toEqual([]); + + expect(fetchImpl).not.toHaveBeenCalled(); + }); + it("fetchIssueStatesByIds filters by Linear ids", async () => { const fetchImpl = vi.fn().mockResolvedValue( jsonResponse({ From ab53e768310963988fd4392b698dff7ba7915e15 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 29 Aug 2026 19:24:23 +0900 Subject: [PATCH 2/5] test(trackers): reject malformed Linear refresh --- .../tracker-linear/src/tracker-linear.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/tracker-linear/src/tracker-linear.test.ts b/packages/tracker-linear/src/tracker-linear.test.ts index d365ce60..8e3f45bd 100644 --- a/packages/tracker-linear/src/tracker-linear.test.ts +++ b/packages/tracker-linear/src/tracker-linear.test.ts @@ -1230,6 +1230,26 @@ Prompt`, }); }); + it("fails when a requested Linear issue has malformed required state data", async () => { + const fetchImpl = vi.fn().mockResolvedValue( + jsonResponse({ + data: { + issues: { + nodes: [linearIssueNode("ENG-123", [], { state: null })], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }, + }) + ); + + await expect( + linearTrackerAdapter.fetchIssueStatesByIds(makeProject(), ["ENG-123"], { + fetchImpl, + token: "linear-token", + }) + ).rejects.toThrow("Linear issue state name is required."); + }); + it("injects worker environment without requiring team id", () => { const env = linearTrackerAdapter.buildWorkerEnvironment( makeProject({ apiUrl: undefined }), From d50b09e224dd54242c088dadd2c36861e9b3530d Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 29 Aug 2026 19:28:12 +0900 Subject: [PATCH 3/5] test(orchestrator): cover safe workspace reconciliation --- packages/orchestrator/src/service.test.ts | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/orchestrator/src/service.test.ts b/packages/orchestrator/src/service.test.ts index cd94ba6a..99b664e4 100644 --- a/packages/orchestrator/src/service.test.ts +++ b/packages/orchestrator/src/service.test.ts @@ -386,6 +386,46 @@ describe("OrchestratorService", () => { expect(snapshot.summary.recovered).toBe(1); }); + it("does not reconcile runs when the project has no active runs", async () => { + const repository = { + owner: "acme", + name: "platform", + cloneUrl: "https://github.com/acme/platform.git", + }; + const projectConfig = createProjectConfig("/tmp/orchestrator", repository); + const store = { + loadProjectIssueOrchestrations: vi.fn().mockResolvedValue([]), + loadAllRuns: vi.fn().mockResolvedValue([]), + saveProjectIssueOrchestrations: vi.fn().mockResolvedValue(undefined), + loadIssueWorkspaces: vi.fn().mockResolvedValue([]), + saveProjectStatus: vi.fn().mockResolvedValue(undefined), + } as unknown as OrchestratorFsStore; + const service = new OrchestratorService(store, projectConfig); + const reconcileRun = vi.fn(); + vi.spyOn( + service as never, + "selectCurrentRunsForReconciliation" + ).mockResolvedValue([]); + vi.spyOn(service as never, "reconcileRun").mockImplementation(reconcileRun); + vi.spyOn(trackerAdapters, "resolveTrackerAdapter").mockImplementation( + () => { + throw new Error("Unsupported tracker adapter: retired-kind"); + } + ); + + const snapshot = await ( + service as unknown as { + reconcileProject( + tenant: OrchestratorProjectConfig + ): Promise; + } + ).reconcileProject(projectConfig); + + expect(reconcileRun).not.toHaveBeenCalled(); + expect(snapshot.summary.recovered).toBe(0); + expect(snapshot.lastError).toContain("Unsupported tracker adapter"); + }); + it("continues dispatching after an earlier candidate fails to start", async () => { process.env.GITHUB_GRAPHQL_TOKEN = "test-token"; const tempRoot = await mkdtemp( @@ -1923,6 +1963,48 @@ describe("OrchestratorService", () => { ); }); + it("fails safely when an issue workspace path is an existing regular file", async () => { + process.env.GITHUB_GRAPHQL_TOKEN = "test-token"; + const tempRoot = await mkdtemp(join(tmpdir(), "orchestrator-workspace-file-")); + const repository = await createRepositoryFixture( + tempRoot, + "acme", + "platform" + ); + const store = new OrchestratorFsStore(tempRoot); + const projectConfig = createProjectConfig(tempRoot, repository); + await store.saveProjectConfig(projectConfig); + const workspaceKey = deriveIssueWorkspaceKey("acme/platform#1"); + const workspacePath = resolveIssueWorkspaceDirectory( + projectConfig.workspaceDir, + workspaceKey + ); + await mkdir(projectConfig.workspaceDir, { recursive: true }); + await writeFile(workspacePath, "preserve this file", "utf8"); + const spawnImpl = vi.fn(); + const service = new OrchestratorService(store, projectConfig, { + fetchImpl: vi.fn().mockResolvedValue(createTrackerResponse(repository)), + spawnImpl: spawnImpl as never, + now: () => new Date("2026-03-08T00:00:00.000Z"), + }); + + const snapshot = await service.runOnce(); + + expect(snapshot.summary.dispatched).toBe(0); + expect(spawnImpl).not.toHaveBeenCalled(); + expect( + await readFile(workspacePath, "utf8") + ).toBe("preserve this file"); + expect( + await store.loadProjectIssueOrchestrations(projectConfig.projectId) + ).toEqual([ + expect.objectContaining({ + identifier: "acme/platform#1", + state: "retry_queued", + }), + ]); + }); + it.each([ { name: "closed source issue", From 35ec89d3a9d168f8181610031325b6a69563fa97 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 29 Aug 2026 19:29:25 +0900 Subject: [PATCH 4/5] docs(testing): map section 17 coverage --- docs/architecture.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index 0611cf2f..5e92b169 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -98,6 +98,20 @@ touches a layer, check that its slice (and the linked documents) still holds. - Runtime state files: `.runtime/orchestrator/` (`workspaces//`, `runs//`) - Instance registry: `${GH_SYMPHONY_INSTANCES_DIR:-${GH_SYMPHONY_CONFIG_DIR:-~/.gh-symphony}/instances/}` (mode `0700`; one file per runtime/project). Daemon runtime overrides do not change this inherited host index. +## §17 conformance test matrix + +The rows below are owned by the focused conformance suites rather than a +single implementation package. They map the upstream test matrix to the +authoritative tests for repository behavior. + +| Spec row | Test mapping | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| §17.2 workspace safety and hooks | `packages/orchestrator/src/service.test.ts` covers rejecting an existing regular file at the issue workspace path and running `after_create` only for a newly created workspace; `packages/core/src/workspace-safety.test.ts` covers path containment. | +| §17.3 empty tracker lookup and malformed refresh | `packages/tracker-{github,linear,file}/src/*test.ts` assert empty state/ID lookups make no provider call. GitHub and Linear suites assert that malformed requested records fail while polling-list malformed items are omitted. | +| §17.4 reconciliation with no running issues | `packages/orchestrator/src/service.test.ts` proves reconciliation does not invoke per-run reconciliation when there are no active runs. | +| §17.5 Codex protocol stream and dynamic-tool rejection | `packages/worker/src/worker-protocol.test.ts` covers protocol writes on stderr; `packages/worker/src/codex-dynamic-tools.test.ts` covers structured rejection of unsupported dynamic tools. | +| §17.7 host port and bind lifecycle | `packages/cli/src/commands/start.test.ts` covers explicit ports and loopback versus `--bind-all` host selection. | + ## Package dependency graph `packages/cli` is the published entrypoint that bundles the rest at build time From 260ae4e85e771dec8a8d3969a4892bdf4209f219 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 29 Aug 2026 20:03:51 +0900 Subject: [PATCH 5/5] docs(testing): correct section 17 conformance map --- AGENT_TEST.md | 36 ++++++++++++----------- docs/architecture.md | 14 ++++----- packages/orchestrator/src/service.test.ts | 18 +++++++----- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/AGENT_TEST.md b/AGENT_TEST.md index 8c36b402..7980b4d1 100644 --- a/AGENT_TEST.md +++ b/AGENT_TEST.md @@ -184,25 +184,27 @@ Control worker behavior with the `STUB_SCENARIO` environment variable: ### Worker lifecycle regression cases -| Case | Automated coverage | Docker black-box confirmation | -| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| API-side lifecycle progress at turn boundaries and the convergence threshold | `packages/worker/src/convergence-lifecycle.test.ts` replicates the per-turn exit and threshold terminal branches with production helpers: confirmed non-actionable readback completes, active state converges, and unavailable readback is an orchestrator failure. `packages/orchestrator/src/service.test.ts` exercises the real final classifier mappings: active schedules continuation, non-actionable succeeds, transient unknown recovers, and persistent unknown emits three cause-bearing deferrals before failure retry with truthful tracker diagnostics; state reads do not reload workflow policy. | `STUB_SCENARIO=api-progress` confirms the successful canonical readback path. `STUB_SCENARIO=api-progress-unknown` specifically removes the canonical item after confirmed progress (the `tracker-item-missing` cause, not an API outage) and requires exactly three persisted `run-finalization-deferred` events with the final event exhausted. | -| Planning phase prompt policy | `packages/worker/src/execution-phase.test.ts`, `packages/core/src/workflow/render.test.ts`, and `packages/cli/src/commands/workflow.test.ts` cover normalized classification and prompt rendering. | `STUB_SCENARIO=prompt-phase` uses a whitespace/case-mismatched `planning_states` entry and fails unless the dispatched prompt contains `phase=planning`. | -| Retry prompt attempt rendering | `packages/orchestrator/src/service.test.ts` covers continuation, queued failure, and recovery retry attempt propagation; `packages/core/src/workflow/render.test.ts` covers integer template rendering. | `STUB_SCENARIO=retry-attempt` completes one actionable turn, then fails unless the continuation worker receives `retry_attempt=1`. | -| Failed worker exit retry classification | `packages/orchestrator/src/service.test.ts` verifies non-zero exit, signal termination, failed-turn, and user-input-required classifications; failure paths retain diagnostics, use exponential backoff, increment failure counts, and append one `run-retried` event with retry diagnostics. | `STUB_SCENARIO=fail` confirms the Docker worker failure path exits non-zero and is retried by the orchestrator; inspect `events.ndjson` for one `run-retried` per scheduled retry and `/api/v1/state` for `issueId`, `attempt`, `error`, `retryKind: "failure"`, and a retained failure diagnostic. | -| Restart failure isolation | `packages/orchestrator/src/service.test.ts` seeds a due retrying run whose restart checkout fails and verifies the failed run/project diagnostics, retained retry backoff, and healthy later-candidate dispatch within the same tick. | TC-17 seeds the due retrying run with an unavailable clone source, performs one refresh, and checks the failed retry diagnostics, future retry entry, and same-tick healthy dispatch. | -| Linear MCP runtime credentials | `packages/tool-linear-graphql/src/tool.test.ts`, `packages/runtime-codex/src/runtime.test.ts`, and `packages/runtime-claude/src/mcp-compose.test.ts` verify that resolved Linear credentials reach the built-in MCP server and API keys are used as raw Authorization values. | The standard Docker `happy` scenario verifies the worker/runtime container path remains healthy; Linear network calls stay unit-covered because E2E uses the isolated file tracker and no live Linear credentials. | -| Codex host-side dynamic tools | `packages/runtime-codex/src/runtime.test.ts`, `packages/worker/src/worker-protocol.test.ts`, `packages/worker/src/codex-dynamic-tools.test.ts`, and `packages/worker/src/codex-initialize.test.ts` cover advertised schemas, the conditional `experimentalApi` initialize capability, conditional `thread/start.dynamicTools`, `item/tool/call` responses, structured failures, and issue-context forwarding. | `docker compose -f docker-compose.e2e.yml exec -T symphony-e2e node /app/e2e/host-dynamic-tool-e2e.mjs` runs the built worker helper through the real provider adapter, stubbing only its HTTP boundary, and verifies one host-side call. For a real Codex smoke, replay the captured `initialize` with `capabilities: { "experimentalApi": true }` before the captured dynamic-tool `thread/start`; Codex must return a thread result rather than error `-32600`. Also replay the no-tools shape with `capabilities: {}` and no `dynamicTools` key; it must return a thread result rather than error `-32600`. | -| Codex turn silence and approval posture | `packages/worker/src/worker-protocol.test.ts` proves every app-server output resets `turn_timeout_ms`, a silent turn is terminated, and an unhandled approval request receives JSON-RPC `-32601`; `codex-policy.test.ts`, `codex-startup.test.ts`, and `workflow-loader.test.ts` prove only `approval_policy: never` can reach startup. | `./e2e/run-e2e.sh happy 60` confirms the Docker worker lifecycle remains healthy after the worker protocol/configuration changes. The actual Codex app-server timing and approval paths are unit-covered because the Docker fixture uses a stub worker. | -| Broker-conditional tracker credential boundary | `packages/runtime-codex/src/runtime.test.ts`, `packages/runtime-claude/src/adapter.test.ts`, and `packages/core/src/runtime/mcp-compose.test.ts` cover brokerless compatibility, brokered raw-token stripping, and `$VAR` source metadata. | `pnpm e2e:claude` launches a broker-configured Claude worker and verifies the child sees no raw GitHub aliases while retaining the broker secret. The standard Docker happy scenario remains the brokerless lifecycle regression path. | -| Workflow reload revision signal | `packages/core/src/workflow-loader.test.ts` proves the revision is short, content-derived, and non-secret; `packages/core/src/observability/snapshot-builder.test.ts` proves snapshots expose the applied revision; `packages/orchestrator/src/service.test.ts` proves dispatch events carry it and that polling/concurrency reload on the next tick. | Start the Docker E2E environment, inject the happy-path issue, then verify `/api/v1/state` has a `workflow.revision` matching `sha256:<12 hex chars>` and the run's `events.ndjson` has the same `workflowRevision` on `run-dispatched`. | -| Tracker issue URL snapshot rows | `packages/core/src/observability/snapshot-builder.test.ts` covers `activeRuns[].issueUrl` and `retryQueue[].issueUrl`; GitHub and Linear adapter suites cover tracker URL normalization; `packages/tracker-file/src/file-tracker-adapter.test.ts` covers fixture URL preservation; `packages/dashboard/src/server.test.ts` and the control-plane render test cover API and row links. | `./e2e/run-e2e.sh happy 60` uses a file-tracker issue URL. While the run is active, `GET /api/v1/state` must expose that URL at `activeRuns[0].issueUrl`; after worker completion, the usual lifecycle assertions still pass. | -| Opaque tracker native reference boundary | `packages/tracker-github/src/tracker-github.test.ts` exercises GitHub linked-PR canonicalization through `resolveCanonicalIssues`; `packages/orchestrator/src/dispatch.test.ts` verifies dispatch uses adapter hooks rather than service-level provider payload inspection. | TC-18 runs the Docker `happy` scenario with the file tracker fixture, which derives its opaque item reference and completes dispatch without provider-specific orchestration branches. | -| Configured repository workflow path and startup preflight | `packages/cli/src/commands/start.test.ts` verifies unsupported tracker kinds fail before daemon construction, project `.env` values resolve during preflight, and missing configured files give a remediation. `packages/orchestrator/src/service.test.ts` verifies repo-typed configured paths are loaded and warn when shadowing the repository root workflow; `doctor.test.ts` and `repo-explain.test.ts` verify diagnostics use the same path. | `./e2e/run-e2e.sh happy 60` confirms the Docker repository-init → repository-start lifecycle and worker dispatch remain healthy after startup-validation changes. The custom-path and invalid-kind edge cases are unit-isolated because the standard Docker fixture uses a fixed repository policy. | -| Adapter-owned dispatch eligibility | `packages/orchestrator/src/dispatch.test.ts` verifies the scheduler suppresses `dispatchable: false` issues, while each tracker adapter's suite verifies its own provider-specific derivation. | `./e2e/run-e2e.sh non-dispatchable 30` injects a file-tracker issue with `dispatchable: false`, waits for two post-injection reconciliation ticks, confirms no worker or `run-dispatched` event exists, and verifies `repo explain` retains the adapter reason. | -| Shared worktree-cache agent branch collection | `packages/orchestrator/src/git.test.ts` verifies that every detached ref under `refs/heads/` in the shared bare cache is deleted only when its tip is reachable from `refs/remotes/origin/*`; unpushed branches and branches linked to live worktrees are retained, including branches from other projects sharing the cache. | The Docker file tracker does not create real agent commits or remote branches; repository-fixture coverage is the authoritative TC for this Git reachability guarantee. | +| Case | Automated coverage | Docker black-box confirmation | +| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| API-side lifecycle progress at turn boundaries and the convergence threshold | `packages/worker/src/convergence-lifecycle.test.ts` replicates the per-turn exit and threshold terminal branches with production helpers: confirmed non-actionable readback completes, active state converges, and unavailable readback is an orchestrator failure. `packages/orchestrator/src/service.test.ts` exercises the real final classifier mappings: active schedules continuation, non-actionable succeeds, transient unknown recovers, and persistent unknown emits three cause-bearing deferrals before failure retry with truthful tracker diagnostics; state reads do not reload workflow policy. | `STUB_SCENARIO=api-progress` confirms the successful canonical readback path. `STUB_SCENARIO=api-progress-unknown` specifically removes the canonical item after confirmed progress (the `tracker-item-missing` cause, not an API outage) and requires exactly three persisted `run-finalization-deferred` events with the final event exhausted. | +| Planning phase prompt policy | `packages/worker/src/execution-phase.test.ts`, `packages/core/src/workflow/render.test.ts`, and `packages/cli/src/commands/workflow.test.ts` cover normalized classification and prompt rendering. | `STUB_SCENARIO=prompt-phase` uses a whitespace/case-mismatched `planning_states` entry and fails unless the dispatched prompt contains `phase=planning`. | +| Retry prompt attempt rendering | `packages/orchestrator/src/service.test.ts` covers continuation, queued failure, and recovery retry attempt propagation; `packages/core/src/workflow/render.test.ts` covers integer template rendering. | `STUB_SCENARIO=retry-attempt` completes one actionable turn, then fails unless the continuation worker receives `retry_attempt=1`. | +| Failed worker exit retry classification | `packages/orchestrator/src/service.test.ts` verifies non-zero exit, signal termination, failed-turn, and user-input-required classifications; failure paths retain diagnostics, use exponential backoff, increment failure counts, and append one `run-retried` event with retry diagnostics. | `STUB_SCENARIO=fail` confirms the Docker worker failure path exits non-zero and is retried by the orchestrator; inspect `events.ndjson` for one `run-retried` per scheduled retry and `/api/v1/state` for `issueId`, `attempt`, `error`, `retryKind: "failure"`, and a retained failure diagnostic. | +| Restart failure isolation | `packages/orchestrator/src/service.test.ts` seeds a due retrying run whose restart checkout fails and verifies the failed run/project diagnostics, retained retry backoff, and healthy later-candidate dispatch within the same tick. | TC-17 seeds the due retrying run with an unavailable clone source, performs one refresh, and checks the failed retry diagnostics, future retry entry, and same-tick healthy dispatch. | +| Linear MCP runtime credentials | `packages/tool-linear-graphql/src/tool.test.ts`, `packages/runtime-codex/src/runtime.test.ts`, and `packages/runtime-claude/src/mcp-compose.test.ts` verify that resolved Linear credentials reach the built-in MCP server and API keys are used as raw Authorization values. | The standard Docker `happy` scenario verifies the worker/runtime container path remains healthy; Linear network calls stay unit-covered because E2E uses the isolated file tracker and no live Linear credentials. | +| Codex host-side dynamic tools | `packages/runtime-codex/src/runtime.test.ts`, `packages/worker/src/worker-protocol.test.ts`, `packages/worker/src/codex-dynamic-tools.test.ts`, and `packages/worker/src/codex-initialize.test.ts` cover advertised schemas, the conditional `experimentalApi` initialize capability, conditional `thread/start.dynamicTools`, `item/tool/call` responses, structured failures, and issue-context forwarding. | `docker compose -f docker-compose.e2e.yml exec -T symphony-e2e node /app/e2e/host-dynamic-tool-e2e.mjs` runs the built worker helper through the real provider adapter, stubbing only its HTTP boundary, and verifies one host-side call. For a real Codex smoke, replay the captured `initialize` with `capabilities: { "experimentalApi": true }` before the captured dynamic-tool `thread/start`; Codex must return a thread result rather than error `-32600`. Also replay the no-tools shape with `capabilities: {}` and no `dynamicTools` key; it must return a thread result rather than error `-32600`. | +| Codex turn silence and approval posture | `packages/worker/src/worker-protocol.test.ts` proves every app-server output resets `turn_timeout_ms`, a silent turn is terminated, and an unhandled approval request receives JSON-RPC `-32601`; `codex-policy.test.ts`, `codex-startup.test.ts`, and `workflow-loader.test.ts` prove only `approval_policy: never` can reach startup. | `./e2e/run-e2e.sh happy 60` confirms the Docker worker lifecycle remains healthy after the worker protocol/configuration changes. The actual Codex app-server timing and approval paths are unit-covered because the Docker fixture uses a stub worker. | +| Broker-conditional tracker credential boundary | `packages/runtime-codex/src/runtime.test.ts`, `packages/runtime-claude/src/adapter.test.ts`, and `packages/core/src/runtime/mcp-compose.test.ts` cover brokerless compatibility, brokered raw-token stripping, and `$VAR` source metadata. | `pnpm e2e:claude` launches a broker-configured Claude worker and verifies the child sees no raw GitHub aliases while retaining the broker secret. The standard Docker happy scenario remains the brokerless lifecycle regression path. | +| Workflow reload revision signal | `packages/core/src/workflow-loader.test.ts` proves the revision is short, content-derived, and non-secret; `packages/core/src/observability/snapshot-builder.test.ts` proves snapshots expose the applied revision; `packages/orchestrator/src/service.test.ts` proves dispatch events carry it and that polling/concurrency reload on the next tick. | Start the Docker E2E environment, inject the happy-path issue, then verify `/api/v1/state` has a `workflow.revision` matching `sha256:<12 hex chars>` and the run's `events.ndjson` has the same `workflowRevision` on `run-dispatched`. | +| Tracker issue URL snapshot rows | `packages/core/src/observability/snapshot-builder.test.ts` covers `activeRuns[].issueUrl` and `retryQueue[].issueUrl`; GitHub and Linear adapter suites cover tracker URL normalization; `packages/tracker-file/src/file-tracker-adapter.test.ts` covers fixture URL preservation; `packages/dashboard/src/server.test.ts` and the control-plane render test cover API and row links. | `./e2e/run-e2e.sh happy 60` uses a file-tracker issue URL. While the run is active, `GET /api/v1/state` must expose that URL at `activeRuns[0].issueUrl`; after worker completion, the usual lifecycle assertions still pass. | +| Opaque tracker native reference boundary | `packages/tracker-github/src/tracker-github.test.ts` exercises GitHub linked-PR canonicalization through `resolveCanonicalIssues`; `packages/orchestrator/src/dispatch.test.ts` verifies dispatch uses adapter hooks rather than service-level provider payload inspection. | TC-18 runs the Docker `happy` scenario with the file tracker fixture, which derives its opaque item reference and completes dispatch without provider-specific orchestration branches. | +| Configured repository workflow path and startup preflight | `packages/cli/src/commands/start.test.ts` verifies unsupported tracker kinds fail before daemon construction, project `.env` values resolve during preflight, and missing configured files give a remediation. `packages/orchestrator/src/service.test.ts` verifies repo-typed configured paths are loaded and warn when shadowing the repository root workflow; `doctor.test.ts` and `repo-explain.test.ts` verify diagnostics use the same path. | `./e2e/run-e2e.sh happy 60` confirms the Docker repository-init → repository-start lifecycle and worker dispatch remain healthy after startup-validation changes. The custom-path and invalid-kind edge cases are unit-isolated because the standard Docker fixture uses a fixed repository policy. | +| Adapter-owned dispatch eligibility | `packages/orchestrator/src/dispatch.test.ts` verifies the scheduler suppresses `dispatchable: false` issues, while each tracker adapter's suite verifies its own provider-specific derivation. | `./e2e/run-e2e.sh non-dispatchable 30` injects a file-tracker issue with `dispatchable: false`, waits for two post-injection reconciliation ticks, confirms no worker or `run-dispatched` event exists, and verifies `repo explain` retains the adapter reason. | +| Shared worktree-cache agent branch collection | `packages/orchestrator/src/git.test.ts` verifies that every detached ref under `refs/heads/` in the shared bare cache is deleted only when its tip is reachable from `refs/remotes/origin/*`; unpushed branches and branches linked to live worktrees are retained, including branches from other projects sharing the cache. | The Docker file tracker does not create real agent commits or remote branches; repository-fixture coverage is the authoritative TC for this Git reachability guarantee. | | Normalized per-state concurrency limits | `packages/core/src/workflow-loader.test.ts` verifies trimmed/lowercased map keys and ignored invalid entries; `packages/orchestrator/src/dispatch.test.ts` verifies a padded mixed-case key caps matching tracker states; `packages/orchestrator/src/explain.test.ts` verifies `repo explain` reports the same mixed-case per-state cap as dispatch. | `./e2e/run-e2e.sh happy 60` confirms the Docker dispatch lifecycle remains healthy with the orchestrator's canonical workflow-state lookup path. | +| §17 conformance coverage | Tracker empty-input and malformed-refresh cases are deterministic adapter tests; workspace-file and no-running reconciliation cases are deterministic orchestrator tests. The authoritative row-to-test map is in `docs/architecture.md` under “§17 conformance test matrix”; Linear malformed polling-item omission and stderr isolation are documented gaps, while `start.test.ts` host/port/bind coverage belongs to §13.7. | `./e2e/run-e2e.sh happy 60` remains the Docker lifecycle confirmation for the file-tracker and workspace path. Run `docker compose -f docker-compose.e2e.yml exec -T symphony-e2e node /app/e2e/host-dynamic-tool-e2e.mjs` for the dynamic-tool boundary. The malformed-provider and regular-file cases remain unit-isolated because the Docker fixture intentionally uses valid file-tracker data and a fresh workspace. | + `docker-compose.e2e.yml` uses `environment.STUB_SCENARIO: ${STUB_SCENARIO:-happy}`, so the scenario can be selected via a shell environment variable. ```bash diff --git a/docs/architecture.md b/docs/architecture.md index 5e92b169..5bb0a12b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -104,13 +104,13 @@ The rows below are owned by the focused conformance suites rather than a single implementation package. They map the upstream test matrix to the authoritative tests for repository behavior. -| Spec row | Test mapping | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| §17.2 workspace safety and hooks | `packages/orchestrator/src/service.test.ts` covers rejecting an existing regular file at the issue workspace path and running `after_create` only for a newly created workspace; `packages/core/src/workspace-safety.test.ts` covers path containment. | -| §17.3 empty tracker lookup and malformed refresh | `packages/tracker-{github,linear,file}/src/*test.ts` assert empty state/ID lookups make no provider call. GitHub and Linear suites assert that malformed requested records fail while polling-list malformed items are omitted. | -| §17.4 reconciliation with no running issues | `packages/orchestrator/src/service.test.ts` proves reconciliation does not invoke per-run reconciliation when there are no active runs. | -| §17.5 Codex protocol stream and dynamic-tool rejection | `packages/worker/src/worker-protocol.test.ts` covers protocol writes on stderr; `packages/worker/src/codex-dynamic-tools.test.ts` covers structured rejection of unsupported dynamic tools. | -| §17.7 host port and bind lifecycle | `packages/cli/src/commands/start.test.ts` covers explicit ports and loopback versus `--bind-all` host selection. | +| Spec row | Test mapping | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| §17.2 workspace safety and hooks | `packages/orchestrator/src/service.test.ts` covers rejecting an existing regular file at the issue workspace path and running `after_create` only for a newly created workspace; `packages/core/src/workspace-safety.test.ts` covers path containment. | +| §17.3 empty tracker lookup and malformed refresh | `packages/tracker-{github,linear,file}/src/*test.ts` assert empty state/ID lookups make no provider call. GitHub and Linear suites assert that malformed requested records fail; GitHub alone covers omission of malformed polling-list items. Linear polling-list omission is a documented implementation gap. | +| §17.4 reconciliation with no running issues | `packages/orchestrator/src/service.test.ts` proves reconciliation does not invoke per-run reconciliation when there are no active runs. | +| §17.5 Codex protocol stream and dynamic-tool rejection | `packages/worker/src/codex-dynamic-tools.test.ts` covers structured rejection of unsupported dynamic tools. Stderr isolation from the protocol stream remains a documented implementation gap pending the S21 decision. | +| §13.7 host, port, and bind lifecycle | `packages/cli/src/commands/start.test.ts` covers explicit ports and loopback versus `--bind-all` host selection. §17.7 positional workflow-path behavior remains a documented divergence. | ## Package dependency graph diff --git a/packages/orchestrator/src/service.test.ts b/packages/orchestrator/src/service.test.ts index 99b664e4..4c4eefd8 100644 --- a/packages/orchestrator/src/service.test.ts +++ b/packages/orchestrator/src/service.test.ts @@ -1965,7 +1965,9 @@ describe("OrchestratorService", () => { it("fails safely when an issue workspace path is an existing regular file", async () => { process.env.GITHUB_GRAPHQL_TOKEN = "test-token"; - const tempRoot = await mkdtemp(join(tmpdir(), "orchestrator-workspace-file-")); + const tempRoot = await mkdtemp( + join(tmpdir(), "orchestrator-workspace-file-") + ); const repository = await createRepositoryFixture( tempRoot, "acme", @@ -1992,9 +1994,7 @@ describe("OrchestratorService", () => { expect(snapshot.summary.dispatched).toBe(0); expect(spawnImpl).not.toHaveBeenCalled(); - expect( - await readFile(workspacePath, "utf8") - ).toBe("preserve this file"); + expect(await readFile(workspacePath, "utf8")).toBe("preserve this file"); expect( await store.loadProjectIssueOrchestrations(projectConfig.projectId) ).toEqual([ @@ -9146,10 +9146,12 @@ Prefer focused changes. dueAt: "2026-03-08T00:00:07.000Z", error: "Worker process exited unexpectedly.", }); - const events = (await readFile( - join(store.runDir("run-1", "tenant-1"), "events.ndjson"), - "utf8" - )) + const events = ( + await readFile( + join(store.runDir("run-1", "tenant-1"), "events.ndjson"), + "utf8" + ) + ) .trim() .split("\n") .map((line) => JSON.parse(line));