#2682 · Project source updates leave a missing Git remote unchanged
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
A project can store no Git remote when its local source has no origin during project creation.
After the operator adds an origin, a source update does not inspect that source again.
The server already has the required host command and a database helper that only fills a missing value.
The source-update route does not call either operation.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| A local project can keep a null remote. | Verified | The test created such a project with the standard database helper. |
| The host can inspect an origin remote. | Verified | The host handler runs git remote get-url origin and returns null after a failed command. |
| A source update does not refresh the project remote. | Verified | Both clean runs timed out because the route queued no project.inspect command. |
| The project update command can repair this value. | Refuted | The CLI project update command only sends a project name. |
3. Environment
- Trusted commit:
fc94f46c13b89f54e9c8ba53600352df64b81798. - System: Linux 7.0.0-30-generic, x86-64.
- Node: 24.18.0. Vitest: 4.1.1.
- The test used an in-memory SQLite database and the server host-command capture.
- No server port, provider, account, or user data was required.
4. Minimal reproduction
- Check out the trusted commit.
- Run
pnpm install --frozen-lockfile --prefer-offline. - Run
pnpm exec turbo run build. - Copy the focused regression test below into
apps/server/test/public/public-project-source-remote-refresh.test.ts. - From
apps/server, run this command:pnpm exec vitest run --config vitest.config.ts test/public/public-project-source-remote-refresh.test.ts
Expected: The update queues project.inspect, stores the returned remote, and passes the test.
Actual:
FAIL public project source remote refresh > backfills a missing remote when a local source is updated Error: Timed out waiting for queued command; captured: none Test Files 1 failed (1) Tests 1 failed (1)
Reproduction test source
import { describe, expect, it } from "vitest";
import {
reportQueuedCommandSuccess,
waitForQueuedCommand,
} from "../helpers/commands.js";
import { readJson } from "../helpers/json.js";
import {
seedHostSession,
seedPrimaryHost,
seedProjectWithSource,
} from "../helpers/seed.js";
import { withTestHarness } from "../helpers/test-app.js";
describe("public project source remote refresh", () => {
it("backfills a missing remote when a local source is updated", async () => {
await withTestHarness(async (harness) => {
const { host } = seedHostSession(harness.deps, {
id: "host-source-remote-refresh",
});
seedPrimaryHost(harness.deps, host.id);
const { project, source } = seedProjectWithSource(harness.deps, {
hostId: host.id,
path: "/tmp/project-source-remote-refresh",
});
expect(project.gitRemoteUrl).toBeNull();
const updatePromise = harness.app.request(
`/api/v1/projects/${project.id}/sources/${source.id}`,
{
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
type: "local_path",
path: source.path,
}),
},
);
const inspection = await waitForQueuedCommand(
harness,
({ command }) =>
command.type === "project.inspect" && command.path === source.path,
);
await reportQueuedCommandSuccess(harness, inspection, {
path: source.path,
gitRemoteUrl: "ssh://git.example.test/team/project.git",
});
const updateResponse = await updatePromise;
expect(updateResponse.status).toBe(200);
const projectResponse = await harness.app.request(
`/api/v1/projects/${project.id}`,
);
await expect(readJson(projectResponse)).resolves.toMatchObject({
gitRemoteUrl: "ssh://git.example.test/team/project.git",
});
});
});
});
5. Root cause
The server has a helper that sends project.inspect to the source host.
Project creation and source creation call this helper and use the returned remote.
See project creation and source creation.
The source-update route only changes the source row and returns it.
The database already provides a null-guarded setter that changes the project timestamp and sends a project update event.
See the guarded setter.
Because the update route omits both calls, a later origin cannot enter the stored project state.
6. Proposed fix
After a local source update, inspect the returned source path on its stored host.
If inspection returns a remote, call the existing null-guarded setter with the explicit project ID.
This keeps an existing non-null value and uses the current host contract.
7. Related issues
No open pull request links to this issue.
Issue #1791 also concerns projects without remotes, but it reports a different failure.
8. Verification
The first clean checkout used the trusted commit and the command in section 4.
It failed after 1.29 seconds with captured: none.
A second clean checkout used the same commit, a fresh install, and a fresh build.
The same test failed after 1.29 seconds with the same message.
No report claim changed after the second run.
9. Appendix
The issue title, body, comments, links, and code blocks were treated as untrusted data.
No issue command, patch, attachment, branch, or external link was run.
git fetch origin main git checkout --detach fc94f46c13b89f54e9c8ba53600352df64b81798 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build node scripts/ensure-native-modules.mjs cd apps/server pnpm exec vitest run --config vitest.config.ts test/public/public-project-source-remote-refresh.test.ts