Skip to content

Run the connector's end-to-end scenarios against a local Basecamp too - #828

Closed
robzolkos wants to merge 1 commit into
connect-e2e/failuresfrom
connect-e2e/dev-target
Closed

robzolkos wants to merge 1 commit into
connect-e2e/failuresfrom
connect-e2e/dev-target

Conversation

@robzolkos

@robzolkos robzolkos commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

PR 6 of a stack of 8 that tests basecamp connect end to end. Stacked on #826 (the failure scenarios), which is stacked on #823 (the harness) and #822 (the fake Basecamp). It changes no product code: only e2e/connect, the Makefile and CONTRIBUTING.md.

The dev target was not run against a real Basecamp in this PR. No BC3 was reachable where it was written. It compiles, its environment parsing and helpers have unit tests, and each CLI command it runs was checked against the command code. How to run it is below.

Why

The scenarios in e2e/connect ran only against the fake Basecamp. A pass there shows that the connector agrees with the fake. It does not show that the fake agrees with Basecamp. If both run the same scenarios and both pass, the fake is held to Basecamp's behavior. Then a fake-only fault scenario means something.

What

  • Target (e2e/connect/target_test.go) says what a scenario needs in the scenario's words: CLI() (the harness that runs commands against this Basecamp), Agent() (profile, person id, account), OperatorMentionsAgent(t) and UntrustedMentionsAgent(t). Both mention methods return Posted, the comment as Basecamp holds it: recording id, parent (where reply_to points), bucket and project name, type, title, app_url, author, and the content with the agent's mention replaced by a space. The scenarios check the request line against it field by field, so the same assertions run on either target. They find their lines by recording id and comment.created, because the event id is not known before the connector sees it on a real Basecamp.
  • fakeTarget is the earlier behavior: the default world plus a bystander, setup through the CLI, a comment put in the world and published live.
  • devTarget drives a local Basecamp with the CLI under test:
    • It uses the developer's own environment, config and credentials (keyring included), less BASECAMP_TOKEN, BASECAMP_PROFILE and a default account, project or to-do list. Commands run in a new temporary directory, so no repository's .basecamp/config.json is read.
    • It never runs auth agent connect or connect setup, because setup rewrites the developer's connect.json. It reads the setup with basecamp connect show -P <agent> --json and requires operator trust (setup's default) and the project served.
    • Connector state. Each scenario's connector gets a temporary XDG_STATE_HOME. The ledger and the per-agent instance lock are there, so a scenario cannot read or move the developer's ledger and cannot hold their lock. That also means the lock cannot keep a scenario's connector apart from the developer's real one. So it first runs basecamp connect status -P <agent> --json against the developer's own state (status takes no lock). If a connector holds the lock, it stops with an error, because that connector would get the scenario's mentions too.
    • Each scenario posts a new message as the operator (messages create --project <id> --no-subscribe), and checks that the author is the person connect.json trusts. It comments with comments create <message> "... [@agent](person:<agent id>)? Thanks". The CLI's Markdown resolves that to a bc-attachment mention, and agents resolve by id. Every command gets -P <profile> --account <agent's account> --json. A cleanup runs messages trash on the message, with its comments. The expected content comes from the comment Basecamp returned. A regex removes its one mention element, so the check does not use the connector's own stripping code.
    • It refuses a BASECAMP_BASE_URL that is not localhost, a loopback address or *.localhost (hostutil.IsLocalhost), because it posts as the developer's profiles. The SDK accepts ws:// cables on the same hosts.
    • Dev scenarios run one at a time behind a mutex: they share one agent, one credential and one project.
  • Scenarios on both targets: a trusted mention handed off in one request line, an untrusted mention discarded as untrusted_performer with nothing on the recording handed off, stdout only NDJSON with stderr saying it runs, SIGINT exits 130, a second connector for the same agent exits 5 at once, and a request admitted under --hold handed off after kill -9, connect release and a restart. In the second-connector scenario, the check that the refused process asked Basecamp for nothing stays on the fake: only the fake sees every request.
  • Fake-only, marked with fakeOnly(t, why): both lanes published apart, the cable down during a commit, a stale cable, a rate-limited token, failed reads, held reads (backlog), a disconnected agent (two scenarios), an agent connected on another computer, two agents side by side, and connect setup itself. When the dev target is requested they skip with their reason, so the run lists what it did not cover.
  • make test-connect-dev checks the five variables like record-cassettes and fails (it does not skip) with the missing one's name. Then it runs BASECAMP_CONNECT_DEV=1 go test -count=1 -failfast -v ./e2e/connect. It builds without -tags dev, as a release builds. Run directly with BASECAMP_CONNECT_DEV=1, the Go side fails too, and names every missing variable at once. A plain go test ./... runs only the fake. The help list has the target.
  • CONTRIBUTING.md: a section "Running basecamp connect against a local Basecamp" with prerequisites, command, what it does to your state, what it covers, and what stays fake-only.
  • Harness gains Dir (where commands run) beside Home, and an internal run(ctx, ...) so a cleanup can run a command after the test's context has ended.

How to run it against your local BC3

Prerequisites: the event feed enabled and Action Cable served; an operator profile logged in as you; a second profile logged in as another person on the project; an agent connected and set up to serve the project in operator trust; no connector running for that agent.

basecamp profile create rob --base-url http://3.basecamp.localhost:3001
basecamp profile create jason --base-url http://3.basecamp.localhost:3001
basecamp auth agent connect -P agent
basecamp connect setup -P agent --operator-profile rob --serve <project-id>

BASECAMP_BASE_URL=http://3.basecamp.localhost:3001 \
BASECAMP_CONNECT_DEV_AGENT_PROFILE=agent \
BASECAMP_CONNECT_DEV_OPERATOR_PROFILE=rob \
BASECAMP_CONNECT_DEV_UNTRUSTED_PROFILE=jason \
BASECAMP_CONNECT_DEV_PROJECT_ID=<project-id> \
make test-connect-dev

Things a first real run may show (each is a real difference, either in the fake or in an assumption here): the event type BC3 sends for a new comment, the pointer line's lane (expected live, because the poll lane is about 30s behind), the comment title and app_url in the request line, and how BC3 writes a mention element in stored content.

Tests

  • New unit tests: devConfigFrom names every missing variable at once and points at CONTRIBUTING.md, reads a valid environment, and refuses a non-numeric or non-positive project id, a non-local or non-http(s) base URL, and two roles on one profile. devEnv keeps the developer's credentials (HOME, XDG_CONFIG_HOME, D-Bus), drops the token, profile and default ids, and replaces only XDG_STATE_HOME. withoutMention removes one mention element, whatever is rendered inside it (a BC3-style figure, or the fake's empty one), and refuses content with no mention or two.
  • make test-connect-dev with BASECAMP_BASE_URL unset exits non-zero with "Set BASECAMP_BASE_URL ...". BASECAMP_CONNECT_DEV=1 go test ./e2e/connect with nothing set fails the dev subtests with the full list of missing variables and skips the fake-only scenarios with their reasons.
  • Not run: the dev target against a real Basecamp.
  • go test -race -count=10 ./e2e/connect passes. bin/ci is green.

Summary by cubic

Runs the connector end-to-end scenarios against a local Basecamp as well as the fake, so the fake's behavior is held against the real one. Only test code, the Makefile, and CONTRIBUTING.md change.

  • The same scenario assertions run on both targets, using a Target interface that describes each Basecamp in the scenario's own words.
  • devTarget drives a local Basecamp with the real CLI and the developer's own config and credentials, isolating only the connector's state (XDG_STATE_HOME) per test.
  • Scenarios requiring faults only the fake can produce call fakeOnly and are skipped on the dev target, with their reason.
  • make test-connect-dev runs the dev target and fails, not skips, when a required environment variable is missing.
  • Scenarios that need a fault the fake can produce (dropped cable, rate-limited token, disconnected agent, second agent) remain fake-only; the dev run lists them as skipped with their reason.

Written for commit 7a8632d. Summary will update on new commits.

Review in cubic

The scenarios in e2e/connect ran only against the fake Basecamp. Nothing
showed that the fake behaves as Basecamp does. Now the same scenarios
also run against a local BC3, so passing on both is the conformance check
between the two.

- Target says what a scenario needs in its own words: the CLI to run,
  the serving agent, the operator mentions the agent, an untrusted person
  mentions the agent. Posted is the comment as Basecamp holds it, so the
  request line is checked field by field on either target.
- fakeTarget is the earlier behavior through fakebasecamp.
- devTarget drives a local Basecamp with the CLI under test and the
  developer's own config and credentials. It reads connect.json with
  `connect show` and never runs setup. Each scenario gets a temporary
  XDG_STATE_HOME (its own ledger and instance lock), a new message
  posted by the operator with nobody subscribed, and comments made with
  `comments create` and `[@agent](person:<id>)`. It trashes the message
  at the end. It refuses to run while `connect status` shows a connector
  running for the agent, and it refuses a BASECAMP_BASE_URL that is not
  local.
- These run on both targets: a trusted mention, an untrusted mention,
  stdout only NDJSON, SIGINT, a second connector refused, and a request
  handed off after --hold, kill -9, release and restart. Scenarios that
  need a fault only the fake can make call fakeOnly with their reason.
  The dev run shows them as skipped.
- `make test-connect-dev` sets BASECAMP_CONNECT_DEV=1 and fails when a
  required variable is missing. A plain `go test` runs only the fake.
- CONTRIBUTING.md tells how to run it and what it covers.
Copilot AI balanced review requested due to automatic review settings October 2, 2026 18:55
@github-actions github-actions Bot added tests Tests (unit and e2e) docs labels Oct 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The dev-target lock check is unreliable, and cleanup can overlap supposedly serialized scenarios.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)
What changed in this PR

Adds a local-Basecamp target for existing connector end-to-end scenarios while retaining fake-only fault testing.

Changes:

  • Introduces shared fake/dev target abstractions.
  • Adds local environment validation, state isolation, and cleanup.
  • Documents and exposes make test-connect-dev.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
Makefile Adds the local connector test target.
CONTRIBUTING.md Documents prerequisites and behavior.
e2e/​connect/​target_test.go Defines shared target abstractions.
e2e/​connect/​dev_target_test.go Implements the local Basecamp target.
e2e/​connect/​fake_target_test.go Adapts the fake to the target interface.
e2e/​connect/​connect_test.go Runs core scenarios across targets.
e2e/​connect/​failures_test.go Shares applicable failure scenarios.
e2e/​connect/​harness_test.go Adds working-directory and cleanup support.
e2e/​connect/​connector_test.go Uses the harness working directory.
e2e/​connect/​main_test.go Updates package documentation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

BucketID: p.BucketID,
ProjectName: p.ProjectName,
RecordingID: p.RecordingID,
Type: "Comment",
Comment on lines +344 to +345
if status.LockHolder != nil && status.LockHolder.PIDStatus == "present" {
t.Fatalf("a connector for %s may be running (pid %d): stop it first, or it is handed the scenarios' mentions too", d.cfg.AgentProfile, status.LockHolder.PID)
Comment on lines +87 to +89
devSerial.Lock()
defer devSerial.Unlock()
scenario(t, newDevTarget(t))

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="e2e/connect/harness_test.go">

<violation number="1" location="e2e/connect/harness_test.go:122">
P3: `Run`'s doc comment still says the command runs "in the harness's home," but commands now run in `h.Dir`; for a devTarget harness `Home` is documented as empty and the CLI runs in the fresh `Dir` with the developer's home. Update the comment to say the harness's directory (`Dir`) so it does not conflate the two fields this change separates.</violation>
</file>

<file name="e2e/connect/dev_target_test.go">

<violation number="1" location="e2e/connect/dev_target_test.go:139">
P3: `devEnv` drops every BASECAMP_* variable that would change who a command acts as or where, but not `BASECAMP_OAUTH_ISSUER`. A developer with that escape hatch pinned (README describes it as a temporary dark-pilot pin) will have every dev-target command authenticate against the wrong issuer — often production — while `BASECAMP_BASE_URL` points at the local Basecamp, failing later and confusingly. Drop it alongside the others so the issuer is discovered from the dev Basecamp URL.</violation>

<violation number="2" location="e2e/connect/dev_target_test.go:226">
P2: This check permits a distinct profile authenticated as the agent. Admission discards agent-authored events before the `untrusted_performer` verdict, so the scenario waits until timeout; also reject `d.agent.PersonID`.</violation>

<violation number="3" location="e2e/connect/dev_target_test.go:334">
P3: This preflight validates a `--json` command's output by substring match: `connect status` returns a structured error envelope (ErrUsageHint JSON), and `require.Contains(stdout+stderr, "has no ledger yet")` matches text inside it. Decode the envelope and assert the message field instead, so a misplaced key or a reworded hint surfaces as a failure rather than a silent pass.</violation>

<violation number="4" location="e2e/connect/dev_target_test.go:344">
P1: Use the actual agent instance lock to gate the dev target instead of relying on `pid_status`; status metadata can be absent or stale, allowing a running connector to consume the scenario mentions concurrently.</violation>
</file>

<file name="CONTRIBUTING.md">

<violation number="1" location="CONTRIBUTING.md:134">
P2: This target only runs on Linux and macOS: the `e2e/connect` package is build-tagged for `linux || darwin`, so Windows contributors following these instructions will not run the scenarios. State the platform prerequisite here.</violation>
</file>

<file name="e2e/connect/connector_test.go">

<violation number="1" location="e2e/connect/connector_test.go:75">
P3: The doc comments directly above `Connect` and `ConnectProfile` still say the command runs "in the harness's home", but this line now starts it in `h.Dir`. For the dev target `Home` is empty and `Dir` is the temporary working directory, so the comments are wrong there. Update both to say the commands run in the harness's working directory.</violation>
</file>

<file name="Makefile">

<violation number="1" location="Makefile:99">
P3: `test-connect-dev` omits `$(BUILD_TAGS)` that every other test recipe passes (`test`, `race-test`, `test-coverage`, etc. all use `$(GOTEST) $(BUILD_TAGS)`). Since `make test` runs `go test -tags dev -v ./...` and therefore includes `./e2e/connect`, the fake-based runs are built with `devBuild=true`, which makes e2e/connect's `binary()` build the basecamp binary with `-tags dev` (dev_tag_test.go, main_test.go). This target builds the test binary without the tag, so against the local Basecamp the connector under test is a release-stub build — the conformance comparison is between two differently-tagged binaries, and any future dev-gated behavior silently diverges. Add `$(BUILD_TAGS)` to the recipe.</violation>
</file>

<file name="e2e/connect/target_test.go">

<violation number="1" location="e2e/connect/target_test.go:88">
P2: The deferred unlock runs before this subtest's cleanups, so another dev scenario can start while the previous connector is still alive or its message is still being trashed. Register the unlock with `t.Cleanup` so target and connector cleanup finishes before the next scenario acquires the mutex.</violation>
</file>

<file name="e2e/connect/connect_test.go">

<violation number="1" location="e2e/connect/connect_test.go:144">
P2: Compare `p.Type` here instead of hardcoding `"Comment"`; otherwise the dev conformance test cannot detect a mismatch between Basecamp’s returned type and the connector’s request line.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

} `json:"lock_holder"`
}
d.data(t, res, &status)
if status.LockHolder != nil && status.LockHolder.PIDStatus == "present" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Use the actual agent instance lock to gate the dev target instead of relying on pid_status; status metadata can be absent or stale, allowing a running connector to consume the scenario mentions concurrently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/dev_target_test.go, line 344:

<comment>Use the actual agent instance lock to gate the dev target instead of relying on `pid_status`; status metadata can be absent or stale, allowing a running connector to consume the scenario mentions concurrently.</comment>

<file context>
@@ -0,0 +1,500 @@
+		} `json:"lock_holder"`
+	}
+	d.data(t, res, &status)
+	if status.LockHolder != nil && status.LockHolder.PIDStatus == "present" {
+		t.Fatalf("a connector for %s may be running (pid %d): stop it first, or it is handed the scenarios' mentions too", d.cfg.AgentProfile, status.LockHolder.PID)
+	}
</file context>

func (d *devTarget) UntrustedMentionsAgent(t *testing.T) Posted {
t.Helper()
p := d.post(t, d.cfg.UntrustedProfile)
require.NotEqual(t, d.operatorID, p.AuthorID, "%s is the operator", envUntrustedProfile)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This check permits a distinct profile authenticated as the agent. Admission discards agent-authored events before the untrusted_performer verdict, so the scenario waits until timeout; also reject d.agent.PersonID.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/dev_target_test.go, line 226:

<comment>This check permits a distinct profile authenticated as the agent. Admission discards agent-authored events before the `untrusted_performer` verdict, so the scenario waits until timeout; also reject `d.agent.PersonID`.</comment>

<file context>
@@ -0,0 +1,500 @@
+func (d *devTarget) UntrustedMentionsAgent(t *testing.T) Posted {
+	t.Helper()
+	p := d.post(t, d.cfg.UntrustedProfile)
+	require.NotEqual(t, d.operatorID, p.AuthorID, "%s is the operator", envUntrustedProfile)
+	return p
+}
</file context>
Suggested change
require.NotEqual(t, d.operatorID, p.AuthorID, "%s is the operator", envUntrustedProfile)
require.NotEqual(t, d.operatorID, p.AuthorID, "%s is the operator", envUntrustedProfile)
require.NotEqual(t, d.agent.PersonID, p.AuthorID, "%s is the agent", envUntrustedProfile)

Comment thread CONTRIBUTING.md
`basecamp connect status -P agent` shows one, because that connector would
get the scenarios' mentions too.

Then run:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This target only runs on Linux and macOS: the e2e/connect package is build-tagged for linux || darwin, so Windows contributors following these instructions will not run the scenarios. State the platform prerequisite here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At CONTRIBUTING.md, line 134:

<comment>This target only runs on Linux and macOS: the `e2e/connect` package is build-tagged for `linux || darwin`, so Windows contributors following these instructions will not run the scenarios. State the platform prerequisite here.</comment>

<file context>
@@ -103,6 +103,79 @@ make test-e2e
+   `basecamp connect status -P agent` shows one, because that connector would
+   get the scenarios' mentions too.
+
+Then run:
+
+```bash
</file context>
Suggested change
Then run:
On Linux or macOS, run:

// One scenario at a time against a local Basecamp: they share its
// agent, its credential and its project.
devSerial.Lock()
defer devSerial.Unlock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The deferred unlock runs before this subtest's cleanups, so another dev scenario can start while the previous connector is still alive or its message is still being trashed. Register the unlock with t.Cleanup so target and connector cleanup finishes before the next scenario acquires the mutex.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/target_test.go, line 88:

<comment>The deferred unlock runs before this subtest's cleanups, so another dev scenario can start while the previous connector is still alive or its message is still being trashed. Register the unlock with `t.Cleanup` so target and connector cleanup finishes before the next scenario acquires the mutex.</comment>

<file context>
@@ -0,0 +1,152 @@
+		// One scenario at a time against a local Basecamp: they share its
+		// agent, its credential and its project.
+		devSerial.Lock()
+		defer devSerial.Unlock()
+		scenario(t, newDevTarget(t))
+	})
</file context>
Suggested change
defer devSerial.Unlock()
t.Cleanup(devSerial.Unlock)

BucketID: p.BucketID,
ProjectName: p.ProjectName,
RecordingID: p.RecordingID,
Type: "Comment",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Compare p.Type here instead of hardcoding "Comment"; otherwise the dev conformance test cannot detect a mismatch between Basecamp’s returned type and the connector’s request line.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/connect_test.go, line 144:

<comment>Compare `p.Type` here instead of hardcoding `"Comment"`; otherwise the dev conformance test cannot detect a mismatch between Basecamp’s returned type and the connector’s request line.</comment>

<file context>
@@ -114,48 +112,55 @@ func TestSetupThroughTheCLIWritesConnectJSON(t *testing.T) {
+			BucketID:    p.BucketID,
+			ProjectName: p.ProjectName,
+			RecordingID: p.RecordingID,
+			Type:        "Comment",
+			Title:       p.Title,
+			URL:         p.URL,
</file context>
Suggested change
Type: "Comment",
Type: p.Type,

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Env = h.env
cmd.Dir = h.Home
cmd.Dir = h.Dir

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Run's doc comment still says the command runs "in the harness's home," but commands now run in h.Dir; for a devTarget harness Home is documented as empty and the CLI runs in the fresh Dir with the developer's home. Update the comment to say the harness's directory (Dir) so it does not conflate the two fields this change separates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/harness_test.go, line 122:

<comment>`Run`'s doc comment still says the command runs "in the harness's home," but commands now run in `h.Dir`; for a devTarget harness `Home` is documented as empty and the CLI runs in the fresh `Dir` with the developer's home. Update the comment to say the harness's directory (`Dir`) so it does not conflate the two fields this change separates.</comment>

<file context>
@@ -98,13 +106,20 @@ type Result struct {
 	cmd := exec.CommandContext(ctx, bin, args...)
 	cmd.Env = h.env
-	cmd.Dir = h.Home
+	cmd.Dir = h.Dir
 	var stdout, stderr bytes.Buffer
 	cmd.Stdout, cmd.Stderr = &stdout, &stderr
</file context>

func (h *Harness) ConnectProfile(t *testing.T, p Profile, extra ...string) *Connector {
t.Helper()
return h.Start(t, h.Home, append([]string{"connect", "-P", p.Name}, extra...)...)
return h.Start(t, h.Dir, append([]string{"connect", "-P", p.Name}, extra...)...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The doc comments directly above Connect and ConnectProfile still say the command runs "in the harness's home", but this line now starts it in h.Dir. For the dev target Home is empty and Dir is the temporary working directory, so the comments are wrong there. Update both to say the commands run in the harness's working directory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/connector_test.go, line 75:

<comment>The doc comments directly above `Connect` and `ConnectProfile` still say the command runs "in the harness's home", but this line now starts it in `h.Dir`. For the dev target `Home` is empty and `Dir` is the temporary working directory, so the comments are wrong there. Update both to say the commands run in the harness's working directory.</comment>

<file context>
@@ -72,7 +72,7 @@ func (h *Harness) Connect(t *testing.T, extra ...string) *Connector {
 func (h *Harness) ConnectProfile(t *testing.T, p Profile, extra ...string) *Connector {
 	t.Helper()
-	return h.Start(t, h.Home, append([]string{"connect", "-P", p.Name}, extra...)...)
+	return h.Start(t, h.Dir, append([]string{"connect", "-P", p.Name}, extra...)...)
 }
 
</file context>

Comment thread Makefile
@test -n "$(BASECAMP_CONNECT_DEV_OPERATOR_PROFILE)" || { echo "Set BASECAMP_CONNECT_DEV_OPERATOR_PROFILE (the agent's operator, logged in); see CONTRIBUTING.md"; exit 1; }
@test -n "$(BASECAMP_CONNECT_DEV_UNTRUSTED_PROFILE)" || { echo "Set BASECAMP_CONNECT_DEV_UNTRUSTED_PROFILE (someone else on the project, logged in); see CONTRIBUTING.md"; exit 1; }
@test -n "$(BASECAMP_CONNECT_DEV_PROJECT_ID)" || { echo "Set BASECAMP_CONNECT_DEV_PROJECT_ID (the served project the scenarios post in); see CONTRIBUTING.md"; exit 1; }
BASECAMP_CONNECT_DEV=1 $(GOTEST) -count=1 -failfast -v ./e2e/connect

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: test-connect-dev omits $(BUILD_TAGS) that every other test recipe passes (test, race-test, test-coverage, etc. all use $(GOTEST) $(BUILD_TAGS)). Since make test runs go test -tags dev -v ./... and therefore includes ./e2e/connect, the fake-based runs are built with devBuild=true, which makes e2e/connect's binary() build the basecamp binary with -tags dev (dev_tag_test.go, main_test.go). This target builds the test binary without the tag, so against the local Basecamp the connector under test is a release-stub build — the conformance comparison is between two differently-tagged binaries, and any future dev-gated behavior silently diverges. Add $(BUILD_TAGS) to the recipe.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Makefile, line 99:

<comment>`test-connect-dev` omits `$(BUILD_TAGS)` that every other test recipe passes (`test`, `race-test`, `test-coverage`, etc. all use `$(GOTEST) $(BUILD_TAGS)`). Since `make test` runs `go test -tags dev -v ./...` and therefore includes `./e2e/connect`, the fake-based runs are built with `devBuild=true`, which makes e2e/connect's `binary()` build the basecamp binary with `-tags dev` (dev_tag_test.go, main_test.go). This target builds the test binary without the tag, so against the local Basecamp the connector under test is a release-stub build — the conformance comparison is between two differently-tagged binaries, and any future dev-gated behavior silently diverges. Add `$(BUILD_TAGS)` to the recipe.</comment>

<file context>
@@ -79,6 +79,25 @@ test: check-toolchain
+	@test -n "$(BASECAMP_CONNECT_DEV_OPERATOR_PROFILE)" || { echo "Set BASECAMP_CONNECT_DEV_OPERATOR_PROFILE (the agent's operator, logged in); see CONTRIBUTING.md"; exit 1; }
+	@test -n "$(BASECAMP_CONNECT_DEV_UNTRUSTED_PROFILE)" || { echo "Set BASECAMP_CONNECT_DEV_UNTRUSTED_PROFILE (someone else on the project, logged in); see CONTRIBUTING.md"; exit 1; }
+	@test -n "$(BASECAMP_CONNECT_DEV_PROJECT_ID)" || { echo "Set BASECAMP_CONNECT_DEV_PROJECT_ID (the served project the scenarios post in); see CONTRIBUTING.md"; exit 1; }
+	BASECAMP_CONNECT_DEV=1 $(GOTEST) -count=1 -failfast -v ./e2e/connect
+
 # Record cassettes for happy-path replay tests.
</file context>
Suggested change
BASECAMP_CONNECT_DEV=1 $(GOTEST) -count=1 -failfast -v ./e2e/connect
BASECAMP_CONNECT_DEV=1 $(GOTEST) $(BUILD_TAGS) -count=1 -failfast -v ./e2e/connect

own := &Harness{t: t, Dir: d.h.Dir, env: devEnv(os.Environ(), "")}
res := own.Run(t, "connect", "status", "-P", d.cfg.AgentProfile, "--json")
if res.ExitCode != 0 {
require.Contains(t, res.Stdout+res.Stderr, "has no ledger yet", "basecamp connect status -P %s\n%s%s", d.cfg.AgentProfile, res.Stdout, res.Stderr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This preflight validates a --json command's output by substring match: connect status returns a structured error envelope (ErrUsageHint JSON), and require.Contains(stdout+stderr, "has no ledger yet") matches text inside it. Decode the envelope and assert the message field instead, so a misplaced key or a reworded hint surfaces as a failure rather than a silent pass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/dev_target_test.go, line 334:

<comment>This preflight validates a `--json` command's output by substring match: `connect status` returns a structured error envelope (ErrUsageHint JSON), and `require.Contains(stdout+stderr, "has no ledger yet")` matches text inside it. Decode the envelope and assert the message field instead, so a misplaced key or a reworded hint surfaces as a failure rather than a silent pass.</comment>

<file context>
@@ -0,0 +1,500 @@
+	own := &Harness{t: t, Dir: d.h.Dir, env: devEnv(os.Environ(), "")}
+	res := own.Run(t, "connect", "status", "-P", d.cfg.AgentProfile, "--json")
+	if res.ExitCode != 0 {
+		require.Contains(t, res.Stdout+res.Stderr, "has no ledger yet", "basecamp connect status -P %s\n%s%s", d.cfg.AgentProfile, res.Stdout, res.Stderr)
+		return
+	}
</file context>
Suggested change
require.Contains(t, res.Stdout+res.Stderr, "has no ledger yet", "basecamp connect status -P %s\n%s%s", d.cfg.AgentProfile, res.Stdout, res.Stderr)
var envelope struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
require.NoError(t, json.Unmarshal([]byte(res.Stdout), &envelope), "basecamp connect status -P %s\n%s%s", d.cfg.AgentProfile, res.Stdout, res.Stderr)
require.Contains(t, envelope.Error.Message, "has no ledger yet", "basecamp connect status -P %s\n%s%s", d.cfg.AgentProfile, res.Stdout, res.Stderr)
return

"BASECAMP_ACCOUNT_ID": true,
"BASECAMP_PROJECT_ID": true,
"BASECAMP_TODOLIST_ID": true,
"BASECAMP_NO_UPDATE_CHECK": true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: devEnv drops every BASECAMP_* variable that would change who a command acts as or where, but not BASECAMP_OAUTH_ISSUER. A developer with that escape hatch pinned (README describes it as a temporary dark-pilot pin) will have every dev-target command authenticate against the wrong issuer — often production — while BASECAMP_BASE_URL points at the local Basecamp, failing later and confusingly. Drop it alongside the others so the issuer is discovered from the dev Basecamp URL.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At e2e/connect/dev_target_test.go, line 139:

<comment>`devEnv` drops every BASECAMP_* variable that would change who a command acts as or where, but not `BASECAMP_OAUTH_ISSUER`. A developer with that escape hatch pinned (README describes it as a temporary dark-pilot pin) will have every dev-target command authenticate against the wrong issuer — often production — while `BASECAMP_BASE_URL` points at the local Basecamp, failing later and confusingly. Drop it alongside the others so the issuer is discovered from the dev Basecamp URL.</comment>

<file context>
@@ -0,0 +1,500 @@
+		"BASECAMP_ACCOUNT_ID":      true,
+		"BASECAMP_PROJECT_ID":      true,
+		"BASECAMP_TODOLIST_ID":     true,
+		"BASECAMP_NO_UPDATE_CHECK": true,
+	}
+	if stateHome != "" {
</file context>
Suggested change
"BASECAMP_NO_UPDATE_CHECK": true,
"BASECAMP_NO_UPDATE_CHECK": true,
"BASECAMP_OAUTH_ISSUER": true,

@robzolkos

Copy link
Copy Markdown
Collaborator Author

Closing: we are not adding another local Basecamp testing target or its dependent harness. The verification skill and existing tests are the chosen tools, with targeted regressions only for actual bugs.

@robzolkos robzolkos closed this Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants