Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2d4a423
feat(harness): add conformance E2E runner (first slice)
ytallo Jul 16, 2026
6c3f0d8
test(conformance): quarantined crash-recovery repro for #507
ytallo Jul 16, 2026
4b4e281
test(conformance): console recording profile + quarantined repros for…
ytallo Jul 16, 2026
f0636da
docs(conformance): runner README with implementation notes
ytallo Jul 16, 2026
a6ebf0d
ci(conformance): run the suite on every PR and test the runner crate
ytallo Jul 16, 2026
d91437e
fix(conformance): make CLI paths absolute before children chdir
ytallo Jul 18, 2026
7bb4271
fix(harness): keep cargo Make targets off the Python scrapling worker
ytallo Jul 18, 2026
4648b78
fix(conformance): truncate mismatch previews on char boundaries
ytallo Jul 18, 2026
036c6ec
fix(conformance): accept slim delta frames per the current router con…
ytallo Jul 18, 2026
0da275e
fix(conformance): error when --scenario all selects nothing
ytallo Jul 18, 2026
a0cb126
fix(conformance): classify RPC errors after a child death as process_…
ytallo Jul 18, 2026
8581c08
fix(conformance): scope the cassette token denylist to credential keys
ytallo Jul 18, 2026
cb2e8fd
test(conformance): pin the recovered turn's shape in C-E2E-507
ytallo Jul 18, 2026
a54b21f
refactor(harness): rename the conformance E2E suite to integration-e2e
ytallo Jul 18, 2026
a91e55a
Merge remote-tracking branch 'origin/main' into feat/harness-conforma…
ytallo Jul 18, 2026
c88aaba
fix(integration): refresh expected prompts after merging main
ytallo Jul 18, 2026
82ee115
(MOT-4107) refactor(harness): simplify integration E2E scenarios and …
ytallo Jul 18, 2026
d0ca3e6
(MOT-4107) refactor(harness): split integration scenario compiler mod…
ytallo Jul 19, 2026
703583f
(MOT-4107) refactor(harness): centralize integration compiler contracts
ytallo Jul 19, 2026
8b6681c
(MOT-4107) refactor(harness): centralize integration runtime ownership
ytallo Jul 19, 2026
1c51f6d
(MOT-4107) refactor(harness): decompose integration scenario runner (…
ytallo Jul 19, 2026
40d05a9
(MOT-4107) refactor(harness): simplify integration E2E scenarios and …
ytallo Jul 20, 2026
1df76d6
fix(harness): resolve(execute) resumes the pre-trigger chain with mut…
ytallo Jul 20, 2026
2136e6d
(MOT-4107) fix(harness): harden integration runner contracts (#545)
ytallo Jul 21, 2026
a49e87c
(MOT-4107) refactor(harness): author integration scenarios as Rust bu…
ytallo Jul 21, 2026
8c8332e
Merge origin/main into feat/harness-conformance-e2e
ytallo Jul 21, 2026
c4a7ca6
Merge refactor/harness-integration-runtime into feat/harness-conforma…
ytallo Jul 21, 2026
aaed72b
(MOT-4107) refactor(integration): drop unused per-scenario timeout se…
ytallo Jul 21, 2026
2e9214c
(MOT-4107) fix(integration): accept the turn-completed terminal flag
ytallo Jul 22, 2026
cb22915
(MOT-4107) refactor(integration): simplify boot readiness and add the…
ytallo Jul 22, 2026
555326f
(MOT-4107) feat(harness): keep multi-turn traces distinct and observable
ytallo Jul 22, 2026
e7e0c81
(MOT-4107) refactor(harness): use traces as integration evidence
ytallo Jul 22, 2026
0ede6af
(MOT-4107) refactor(harness): replace integration polling with events
ytallo Jul 22, 2026
785c995
(MOT-4107) fix(harness): stabilize integration E2E validation
ytallo Jul 22, 2026
ae41b45
(MOT-4107) chore: merge main into harness integration runner
ytallo Jul 22, 2026
967544c
(MOT-4107) test(console): stabilize integration E2E interactions
ytallo Jul 22, 2026
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
204 changes: 204 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,210 @@ jobs:
kill "$(cat iii-engine.pid)" 2>/dev/null || true
fi

# ──────────────────────────────────────────────────────────────
# Harness integration E2E (non-required): deterministic public-path
# regression scenarios against the engine pinned in
# harness/tests/e2e/engine.lock (see that crate's README; the
# architecture spec lives in the iii repo). Runs on EVERY PR — the
# suite is the harness stack's public-contract gate, and engine/worker
# builds are cached. The engine is BUILT from the pinned source, never
# downloaded; a cache hit is still re-hashed before use. "Non-required"
# is enforced by branch protection, not continue-on-error.
# ──────────────────────────────────────────────────────────────
harness-integration:
name: harness integration e2e
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Read and validate engine.lock
id: lock
run: |
set -euo pipefail
lock=harness/tests/e2e/engine.lock
read_field() {
local field=$1 values
values=$(sed -n "s/^${field} = \"\\([^\"]*\\)\"$/\\1/p" "$lock")
[[ $(printf '%s\n' "$values" | sed '/^$/d' | wc -l) -eq 1 ]] || {
echo "engine.lock: $field must appear exactly once"
exit 3
}
printf '%s' "$values"
}
invalid=$(sed \
-e '/^[[:space:]]*$/d' \
-e '/^[[:space:]]*#/d' \
-e '/^\(repository\|revision\|package\|binary\) = "[^"]*"$/d' \
"$lock")
[[ -z "$invalid" ]] || {
echo "engine.lock: unsupported or malformed entry"
printf '%s\n' "$invalid"
exit 3
}
repository=$(read_field repository)
revision=$(read_field revision)
package=$(read_field package)
binary=$(read_field binary)
[[ "$repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || {
echo "engine.lock: repository must be an owner/name slug"
exit 3
}
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || { echo "engine.lock: revision must be 40-hex"; exit 3; }
[[ "$package" =~ ^[A-Za-z0-9_-]+$ ]] || {
echo "engine.lock: package contains unsupported characters"
exit 3
}
[[ "$binary" =~ ^target/(debug|release)/[A-Za-z0-9_.-]+$ ]] || {
echo "engine.lock: binary must be a target profile path"
exit 3
}
{
echo "repository=$repository"
echo "revision=$revision"
echo "package=$package"
echo "binary=$binary"
} >> "$GITHUB_OUTPUT"

- name: Checkout pinned engine source
uses: actions/checkout@v4
with:
repository: ${{ steps.lock.outputs.repository }}
ref: ${{ steps.lock.outputs.revision }}
path: target/integration-engine-src

- uses: dtolnay/rust-toolchain@stable

- uses: pnpm/action-setup@v4
with:
version: 11.13.1

- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: console/web/pnpm-lock.yaml

- name: Cache engine build
uses: actions/cache@v4
with:
path: target/integration-engine-src/target
key: integration-engine-${{ runner.os }}-${{ runner.arch }}-${{ steps.lock.outputs.revision }}-${{ hashFiles('target/integration-engine-src/Cargo.lock') }}

- name: Build pinned engine
working-directory: target/integration-engine-src
run: cargo build --locked --release -p ${{ steps.lock.outputs.package }}

- name: Record engine digest
id: engine
run: |
set -euo pipefail
bin="$GITHUB_WORKSPACE/target/integration-engine-src/${{ steps.lock.outputs.binary }}"
[[ -x "$bin" ]] || { echo "engine binary missing at $bin"; exit 3; }
sha256sum "$bin"
echo "bin=$bin" >> "$GITHUB_OUTPUT"

- uses: Swatinem/rust-cache@v2
with:
workspaces: |
harness -> target
queue -> target
session-manager -> target
context-manager -> target
iii-directory -> target
console -> target

- name: Integration crate unit tests
run: cargo test --manifest-path harness/Cargo.toml -p harness-integration

- name: Validate integration scenarios
run: make -C harness integration-validate

- name: Run integration scenarios
run: make -C harness integration-e2e III_BIN="${{ steps.engine.outputs.bin }}"

- name: Install Console web dependencies
working-directory: console/web
run: pnpm install --frozen-lockfile

- name: Build Console web app
working-directory: console/web
run: pnpm build

- name: Build Console worker
run: cargo build --release --manifest-path console/Cargo.toml

- name: Install Playwright Chromium
working-directory: console/web
run: pnpm exec playwright install --with-deps chromium

- name: Typecheck Console E2E tests
working-directory: console/web
run: pnpm typecheck:e2e

- name: Run Console Playwright scenarios
working-directory: console/web
env:
III_BIN: ${{ steps.engine.outputs.bin }}
HARNESS_INTEGRATION_BIN: ${{ github.workspace }}/harness/target/release/harness-integration
HARNESS_BIN: ${{ github.workspace }}/harness/target/release/harness
CONSOLE_BIN: ${{ github.workspace }}/console/target/release/console
QUEUE_BIN: ${{ github.workspace }}/queue/target/release/queue
III_DIRECTORY_BIN: ${{ github.workspace }}/iii-directory/target/release/iii-directory
SESSION_MANAGER_BIN: ${{ github.workspace }}/session-manager/target/release/session-manager
CONTEXT_MANAGER_BIN: ${{ github.workspace }}/context-manager/target/release/context-manager
CONSOLE_E2E_ARTIFACTS_DIR: ${{ github.workspace }}/target/console-e2e
run: pnpm test:e2e

- name: Verify integration report links
run: |
set -euo pipefail
shopt -s nullglob
reports=(target/integration/*/execution.json)
((${#reports[@]} > 0)) || { echo "no execution reports found"; exit 3; }
for execution in "${reports[@]}"; do
run_dir=$(dirname "$execution")
result_path=$(jq -er '.result_path' "$execution")
[[ "$result_path" == "result.json" ]] || {
echo "$execution: unexpected result_path $result_path"
exit 3
}
expected=$(jq -er '.result_sha256' "$execution")
actual=$(sha256sum "$run_dir/$result_path" | cut -d' ' -f1)
[[ "$actual" == "$expected" ]] || {
echo "$execution: result digest mismatch"
exit 3
}
done

- name: Upload compact results
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-results
path: |
target/integration/*/result.json
target/integration/*/execution.json
target/integration/*/teardown.json
if-no-files-found: ignore

- name: Upload full non-pass artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-artifacts
path: target/integration/
retention-days: 14
if-no-files-found: ignore

- name: Upload Console E2E artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: console-e2e-artifacts
path: target/console-e2e/
retention-days: 14
if-no-files-found: ignore

# ──────────────────────────────────────────────────────────────
# Node per-worker lint (biome) + test
# ──────────────────────────────────────────────────────────────
Expand Down
44 changes: 44 additions & 0 deletions console/web/e2e/durable-hydration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { expect, expectPassingResult, openSession, test } from './harness-stack'

test.use({ scenario: 'streamed-text' })

test('hydrates a durable transcript again after a page reload', async ({
page,
stack,
}) => {
const completed = stack.waitForTurnCompleted()
await stack.trigger()
expect(await completed).toMatchObject({ status: 'completed' })

await openSession(page, stack)
await expect(
page.locator('[data-message-role="user"]', {
hasText: stack.ready.message,
}),
).toHaveCount(1)
await expect(
page.locator('[data-message-role="assistant"]', {
hasText: 'fixture complete',
}),
).toHaveCount(1)

await page.reload()
await page
.getByRole('button', {
name: `open ${stack.ready.session.title}`,
exact: true,
})
.click()
await expect(
page.locator('[data-message-role="user"]', {
hasText: stack.ready.message,
}),
).toHaveCount(1)
await expect(
page.locator('[data-message-role="assistant"]', {
hasText: 'fixture complete',
}),
).toHaveCount(1)

expectPassingResult(await stack.finish())
})
48 changes: 48 additions & 0 deletions console/web/e2e/exactly-once-function.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, expectPassingResult, openSession, test } from './harness-stack'

test.use({ scenario: 'exactly-once-function' })

test('renders one completed function call and its durable result', async ({
page,
stack,
}) => {
const completed = stack.waitForTurnCompleted()
await stack.trigger()
expect(await completed).toMatchObject({ status: 'completed' })

await openSession(page, stack)
const functionId = stack.ready.functions.record
expect(functionId).toBeTruthy()
const card = page.locator('[data-message-role="function-call"]', {
hasText: functionId,
})
await expect(card).toHaveCount(1)
await expect(card).toHaveAttribute('data-function-id', functionId)
await expect(card).toHaveAttribute('data-function-status', 'done')

const cardHeader = card.getByRole('button', { name: functionId })
await expect(cardHeader).toHaveAttribute('aria-expanded', 'false')
await cardHeader.click()
await expect(cardHeader).toHaveAttribute('aria-expanded', 'true')

const requestPane = card.locator('[data-function-pane="request"]')
await expect(requestPane).toBeVisible()
await expect(requestPane).toContainText('request · value')
await expect(requestPane.locator('code')).toHaveText('expected')

const responsePane = card.locator('[data-function-pane="response"]')
await expect(responsePane).toBeVisible()
await expect(responsePane).toContainText('recorded')
await expect(responsePane).toContainText('details')
await expect(responsePane).toContainText(/"text"\s*:\s*"recorded"/)
await expect(responsePane).toContainText(/"is_error"\s*:\s*false/)

await expect(
page.locator('[data-message-role="assistant"]', {
hasText: 'recorded once',
}),
).toHaveCount(1)

const result = await stack.finish()
expectPassingResult(result)
})
Loading
Loading