Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .github/workflows/product-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ on:
permissions:
contents: read

env:
PYTHONUTF8: "1"
THOTH_WORKSPACE: .thoth-ci
THOTH_IRIS_RESULT_FIXTURE: .thoth-ci/no-private-result.json

jobs:
product-check:
# The documented quick start supports Windows; Linux remains unverified.
checks:
# Fast public checks (about ten minutes). The full Python suite takes hours on a hosted
# runner, so maintainers run it locally before merging.
runs-on: windows-latest
timeout-minutes: 180
timeout-minutes: 60
defaults:
run:
shell: pwsh
env:
PYTHONUTF8: "1"
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.thoth-test-browsers
THOTH_WORKSPACE: .thoth-ci
THOTH_IRIS_RESULT_FIXTURE: .thoth-ci/no-private-result.json
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
Expand All @@ -45,8 +48,6 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
.\.venv\Scripts\python.exe -m playwright install --only-shell chromium
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Python product tests
run: .\.venv\Scripts\python.exe -m pytest
- name: Python lint
run: .\.venv\Scripts\python.exe -m ruff check src apps tests migrations conftest.py scripts/pytest_environment.py
- name: Runtime architecture
Expand Down
39 changes: 31 additions & 8 deletions apps/web/src/components/FirstRunSetup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ const fixture = vi.hoisted(() => ({
deferredStatus: null as null | (() => void),
holdStatus: false,
forbidNetwork: false,
// Requests the fake server has received and not yet answered; the tests wait for it to reach 0.
inflight: 0,
}));

vi.mock("../api/rpcClient", async (importOriginal) => {
const original = await importOriginal<typeof import("../api/rpcClient")>();
return {
...original,
rpc: async (method: string, input: Record<string, unknown>) => {
const answer = async (method: string, input: Record<string, unknown>) => {
fixture.calls.push({ method, input });
if (method === "workspace/ready") {
if (fixture.readyError) throw new Error(fixture.readyError);
Expand Down Expand Up @@ -103,7 +103,18 @@ vi.mock("../api/rpcClient", async (importOriginal) => {
return { state: "SUCCEEDED", operation_id: "", value: fixture.ready };
}
throw new Error(`unexpected method ${method}`);
},
};
return {
...original,
rpc: async (method: string, input: Record<string, unknown>) => {
// A response the test deliberately holds back is not waited for.
const held = (method === "workspace/ready" && fixture.deferReady)
|| (method === "model/credential/login/status" && fixture.holdStatus)
|| (method === "model/tooling/install" && fixture.installGate !== null);
if (held) return answer(method, input);
fixture.inflight += 1;
try { return await answer(method, input); } finally { fixture.inflight -= 1; }
},
};
});

Expand Down Expand Up @@ -186,9 +197,19 @@ it("keeps a normal MISSING setup state on the existing first-run path", async ()
expect(container.textContent).not.toContain("기존 작업 공간 설정을 확인하세요");
expect(fixture.calls.some(call => call.method === "workspace/setup/update")).toBe(false);
});
/** Lets the screen show what the fake server answered: waits until every request has been answered
* and no new one starts for several ticks in a row (not a fixed time, which a slow machine overruns;
* and not a single idle reading, because the screen sends the next request a moment after a response). */
async function flush() {
await act(async () => {
await tick();
let quiet = 0;
let seen = fixture.calls.length;
while (quiet < 3) {
await tick();
const idle = fixture.inflight === 0 && fixture.calls.length === seen;
seen = fixture.calls.length;
quiet = idle ? quiet + 1 : 0;
}
});
}

Expand Down Expand Up @@ -238,6 +259,7 @@ afterEach(async () => {
fixture.installGate = null;
fixture.deferredStatus = null;
fixture.holdStatus = false;
fixture.inflight = 0;
fixture.forbidNetwork = false;
vi.restoreAllMocks();
vi.unstubAllGlobals();
Expand Down Expand Up @@ -444,7 +466,7 @@ it("ignores a late xAI status response after the workspace changes", async () =>
fixture.holdStatus = true;
await act(async () => { [...row.querySelectorAll("button")].find(button => button.textContent === "상태 직접 확인")!.click(); });
await flush();
expect(fixture.deferredStatus).not.toBeNull();
await vi.waitFor(() => expect(fixture.deferredStatus).not.toBeNull());
fixture.ready = { ...fixture.ready, workspace_id: WORKSPACE_B };
await act(async () => { await client.invalidateQueries({ queryKey: ["workspace-ready"] }); });
await flush();
Expand Down Expand Up @@ -496,8 +518,9 @@ it("stops xAI device polling when its Unix-seconds expiry passes", async () => {
fixture.forbidNetwork = true;
fixture.ready = localReady();
fixture.accounts = defaultAccounts().map((account) => account.provider === "xai" ? xaiAccount() : account);
fixture.registerResult = xaiStart(0.3);
await mount();
// The short expiry runs from the start response, so it is set after the slow mount, not before.
fixture.registerResult = xaiStart(0.3);
const row = [...container.querySelectorAll(".first-run-provider")].find(item => item.textContent?.includes("xAI"))!;
await act(async () => [...row.querySelectorAll("button")].find(button => button.textContent === "xAI로 로그인")!.click()); await flush();
await act(async () => { await new Promise(resolve => setTimeout(resolve, 1100)); });
Expand Down Expand Up @@ -791,7 +814,7 @@ it("ignores a late Codex OAuth status from the previous workspace", async () =>
fixture.holdStatus = true;
await act(async () => [...row.querySelectorAll("button")].find(button => button.textContent === "상태 직접 확인")!.click());
await flush();
expect(fixture.deferredStatus).not.toBeNull();
await vi.waitFor(() => expect(fixture.deferredStatus).not.toBeNull());
fixture.ready = localReady({ workspace_id: WORKSPACE_B });
await act(async () => { await client.invalidateQueries({ queryKey: ["workspace-ready"] }); });
await flush();
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/test_model_execution_marks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""A real request that the provider refuses for the model or account marks that model; nothing else."""
"""A real request that the provider refuses for the model or account marks that model.

Nothing else is marked.
"""

from __future__ import annotations

Expand Down
5 changes: 4 additions & 1 deletion tests/integration/test_scope_read_request_memo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Inside one query the read-approval memo gives the same verdicts as no memo, with far fewer lookups."""
"""Inside one query the read-approval memo gives the same verdicts as no memo.

It does so with far fewer lookups.
"""

from __future__ import annotations

Expand Down
Loading