chore: improve harness guidance and ACP debugging (#1969)
This commit is contained in:
@@ -146,6 +146,8 @@ replace state verification, installation association, or HMAC verification.
|
||||
|
||||
**Agent Runtime** (`internal/agent/runtime/`) is the single seam for launching, resuming, stopping, and observing agent executions. ADR 0004 introduced this in Phase 1 of task-model-unification. The public surface is `runtime.Runtime` (`runtime.go`); a thin facade (`facade.go`) delegates to a `Backend` (satisfied by `*lifecycle.Manager`).
|
||||
|
||||
**Runtime environment invariant:** `Agent.Runtime().Env` applies to every ACP subprocess entry point. Route new overrides through host-utility probes and sessionless prompts into agentctl child processes before sanitization; cover probe DTO, prompt DTO, and child-process boundaries.
|
||||
|
||||
**Convention:** only `internal/agent/runtime/` (and code that pre-dates Phase 1 migration) may import `runtime/lifecycle` or `runtime/agentctl` directly. New consumers — workflow engine actions, cron-driven trigger handlers, future task-tier callers — should depend on `runtime.Runtime`. Existing call sites are migrated through later phases of task-model-unification.
|
||||
|
||||
**Lifecycle Manager** (`internal/agent/runtime/lifecycle/`) manages agent instances under the runtime:
|
||||
@@ -159,6 +161,11 @@ replace state verification, installation association, or HMAC verification.
|
||||
|
||||
**agentctl client** (`internal/agent/runtime/agentctl/`) is the HTTP/WS client used by the lifecycle manager to talk to a running agentctl instance. It is a runtime-tier package and should not be imported outside `internal/agent/runtime/`.
|
||||
|
||||
**Agent discovery vs. ACP probing:** discovery answers only whether an agent
|
||||
executable is available. Authentication, protocol compatibility, and supported
|
||||
models or modes belong to the ACP probe path; do not reintroduce those checks
|
||||
as installation gates.
|
||||
|
||||
**agentctl** is an HTTP server that:
|
||||
- Runs inside Docker containers or as standalone process
|
||||
- Manages agent subprocess via stdin/stdout (ACP protocol)
|
||||
@@ -215,6 +222,7 @@ Client (WS) ← Orchestrator ← Lifecycle Manager ←──── stream update
|
||||
|
||||
- Provider pattern for DI; stderr for logs, stdout for ACP only.
|
||||
- Pass context through chains; event bus for cross-component comm.
|
||||
- **Event-bus wildcard parity:** New NATS wildcard subscriptions must verify equivalent `MemoryEventBus` semantics in `go test ./internal/events/bus`.
|
||||
- **Repository provider identity:** Provider-backed repositories are keyed by workspace, provider, normalized `provider_host` origin, full owner/namespace, and name. Persist `provider_host` when importing or resolving a remote; do not infer self-managed GitLab rows from owner/name alone. Legacy rows with an empty host have unknown identity and must fail closed for provider write/link operations.
|
||||
- **Execution access:** Workspace-oriented handlers (files, shell, inference, ports, vscode, LSP) MUST use `GetOrEnsureExecution(ctx, sessionID)` — it recovers from backend restarts by creating executions on-demand. Only use `GetExecutionBySessionID` for operations that require a running agent process (prompt, cancel, mode).
|
||||
- **Task lifecycle events:** Any code path that mutates a task row must publish via the event bus (`task.created` / `task.updated` / `task.deleted`) — either by going through `Service.CreateTask` / `UpdateTask` / `DeleteTask` / `ArchiveTask`, or by calling `publishTaskEvent` (or one of the `Publish*` helpers in `service_events.go`) directly. Walking `repository.TaskRepository` straight bypasses event publishing and breaks WS-driven UI like the All-Workflows kanban view. `HandoffService`'s cascade methods learned this the hard way — they now require a `TaskEventPublisher` wired via `SetTaskEventPublisher`. New cascade / bulk / cleanup paths must follow the same pattern.
|
||||
@@ -223,7 +231,19 @@ Client (WS) ← Orchestrator ← Lifecycle Manager ←──── stream update
|
||||
- **Joining production goroutines in tests:** When code spawns untracked goroutines (e.g. `fireWakeup`), don't rely on arbitrary sleeps. Join via an observable side effect — e.g. block on `EventTypeComplete` from `a.updatesCh` after unblocking the fake agent. Use short timeouts (~100ms) for in-process negative assertions; reserve multi-second waits for subprocess/integration tests only.
|
||||
- **Path/security tests:** Avoid using the real filesystem root as a fixture root. Build fake absolute roots under `t.TempDir()` with `filepath.Join`; this keeps tests portable across Windows, POSIX, and privileged cloud executors.
|
||||
- **Filesystem permission tests:** Assert permission-denied behavior only after probing that the current executor enforces the permission bit change. Root-like Sprite executors may bypass `chmod` restrictions.
|
||||
- **Filesystem safety checks:** Carry the original `os.Lstat` `FileInfo` (or opened handle) through every decision; do not re-stat a validated path, which reopens a TOCTOU window before side effects. Test mismatches before writes or manager commands.
|
||||
- **Git indexed-environment tests:** When a test supplies `GIT_CONFIG_COUNT`,
|
||||
`GIT_CONFIG_KEY_n`, and `GIT_CONFIG_VALUE_n`, clear every inherited indexed
|
||||
key first and restore it with `t.Cleanup`. Changing only the count can leave
|
||||
higher parent indexes behind and create a malformed Git config block.
|
||||
- **Full test output:** For local full-suite pass/fail validation, prefer plain `go test -race ./...`. `go test -json ./...` can emit very large JSONL streams; if a wrapper or tracing tool truncates the stream mid-record, downstream JSON parsing may fail even when Go tests passed. Use JSON output mainly for CI artifacts or test-report tooling that explicitly requires it.
|
||||
- **Private executable helpers:** When library code starts `os.Executable()` in
|
||||
a private helper mode, do not rely only on a command package's dispatch or
|
||||
package-specific `TestMain`: an importing package's test binary can be the
|
||||
executable and recursively run tests. Use either an explicitly marked
|
||||
private env-and-argument trampoline that runs before normal test entry, or
|
||||
inject a dedicated helper executable. Cover it from a different importing
|
||||
package, not only the helper package.
|
||||
|
||||
### Goroutine ownership and leak testing
|
||||
|
||||
@@ -247,6 +267,8 @@ Every long-running goroutine must have a single owner with explicit start and st
|
||||
|
||||
## Schema & migrations (SQLite repository)
|
||||
|
||||
`internal/task/repository/sqlite` also runs against PostgreSQL: avoid unguarded SQLite-only `rowid`, JSON, or date syntax and add an environment-gated PostgreSQL behavior test for every changed dialect-sensitive method (schema replay is insufficient).
|
||||
|
||||
`initSchema()` in `internal/task/repository/sqlite/base_schema.go` runs the `init*Schema` (CREATE TABLE) steps **before** `runMigrations()`. The table-creation DDL uses `CREATE TABLE IF NOT EXISTS`, so on an **existing** database it is a no-op and never adds columns to a table that is already present.
|
||||
|
||||
**Rule:** when you add a column to an existing table, add it **only** via an idempotent `ADD COLUMN` migration in `runMigrations()` (`base_migrations.go`), never by editing the table's `CREATE TABLE` alone. Anything that *references* that new column — an index, a backfill `UPDATE`, a partial-index predicate — must live in `runMigrations()` **after** the `ADD COLUMN`, not in the `init*Schema` DDL. Putting a `CREATE INDEX ... (new_col)` in the schema-init block crashes existing DBs with `no such column: new_col`, because schema init runs before the migration that adds the column.
|
||||
|
||||
Reference in New Issue
Block a user