✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

✨ Register <TestAgent> and <TestAgent.Scenario> as function components - #270

Merged
taras merged 3 commits into
mainfrom
feat/test-agent-components
Aug 2, 2026
Merged

✨ Register <TestAgent> and <TestAgent.Scenario> as function components#270
taras merged 3 commits into
mainfrom
feat/test-agent-components

Conversation

@taras

@tarastaras commented Aug 2, 2026

Copy link
Copy Markdown
Owner

#202. packages/test-agent was the last package claiming ordinary component names through Component.expand.

The migration

Both register non-reserved, so a repository component replaces either. The dotted name addresses a subdirectory (probeComponentPath does name.replace(/\./g, "/")), so that override is components/TestAgent/Scenario.md.

Every prop was already a string literal, so all become schema-validated props: neither component declares a capture, and neither relies on the by-reference return default<TestAgent> returns its rendered body text, <TestAgent.Scenario> returns "". This slice consumes the post-#265 surface only in that returning unknown is now the signature.

<TestAgent.Scenario> reads its declaring path from invocation().position, which is all it ever needed from the element.

<TestAgent> uses the <Testing> completion shape rather than content(): a body may legally hold a settled diagnostic beside healthy scenarios, and content() would replace the invocation's output with those segments (CF19 pins that). tryContent()'s text keeps them inline exactly as the segments it replaced did — TC1 is the behavioural precedent. A body that genuinely stopped still travels on untouched, per #247.

The finding: content anchors to the invocation, not to a child frame

The migration failed at first with the body rendering nothing at all. Diagnosis, in order:

  1. A marker at the top of the fn printed 12 times — resolution and registration were fine.
  2. A bare-harness repro showed content() empty even for trivial children. That repro was invalid — the harness lacked the ExpansionFrame provider — and it produced no core change. Worth recording, because concluding an engine gap from it would have repeated an earlier wrong turn.
  3. A runDoc repro with scoped() + tryContent() rendered fine, and stayed fine through the whole region — controller, policy, Agent.around. It was under-shaped: its body was plain text needing no context.
  4. Instrumenting the real component gave the answer: failure=MissingContextError: testAgent.session, then, after switching to get(), a body reporting "is valid only inside <TestAgent>" and "Agent.agent() has no provider".

Content projected by tryContent() anchors to the invocation, not to a scoped() inside the fn — so a context set or middleware installed in a nested frame is invisible to the body. The legacy scoped() existed because a claimed element was not an invocation; now it is, and the invocation is already the bound the region needs. No engine change: this is the slice using the boundary correctly.

Also TestAgentContext is read with get() rather than expect(). It is declared TestAgentSession | undefined with an undefined default and the line below already branches on undefined, so expect() could only ever throw where the code meant to branch.

Scope

The Component.expand install is kept with no arms — retiring the surface is the legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside the worker's own execution (worker/run.ts:185) around a scenario document rather than the user's. Flagged so the "count becomes zero" audit accounts for it.

Verification

Deno 2.9.1: fmt, lint (0 errors), check, test (314 passed / 0 failed), check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build + compiled-binary smoke, git diff --check, node.ts mode checked after the build. All 15 test-agent suites pass unchanged — no test needed updating, so there is no suite-parity claim to audit.

The last package claiming ordinary component names through Component.expand.
Both register non-reserved, so a repository component replaces either — the
dotted name addresses a subdirectory, so that override is
components/TestAgent/Scenario.md. Every prop was already a string literal, so
all of them become schema-validated props: neither component declares a capture,
and neither relies on the by-reference return default.
<TestAgent.Scenario> reads its declaring path from invocation().position, which
is all it ever needed from the element.
<TestAgent> uses the <Testing> completion shape rather than content(): a body may
legally hold a settled diagnostic beside healthy scenarios, and content() would
replace the invocation's output with those segments, where tryContent()'s text
keeps them inline exactly as the segments it replaced did. A body that genuinely
stopped still travels on untouched.
The region is deliberately NOT wrapped in scoped(). Content projected by
tryContent() anchors to the invocation, not to a child frame, so a context set
or middleware installed inside a nested scoped() is invisible to the body —
which is what made every scenario report "is valid only inside <TestAgent>" and
every prompt "Agent.agent() has no provider". The invocation is already the bound
the region needs. The legacy scoped() existed because a claimed element was not
an invocation; now it is.
Also: TestAgentCtx reads with get() rather than expect(). It is declared
`TestAgentSession | undefined` with an undefined default, and the line below it
already handles undefined, so expect() could only ever throw where the code
meant to branch.
The Component.expand install is kept with no arms; retiring the surface is the
legacy-removal slice's job, together with <WhenPrompt>, which is claimed inside
the worker's own execution.
@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown

PR #270: ✨ Register <TestAgent> and <TestAgent.Scenario> as function components

3 files, +194 / -51

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable defaultAgent in TestAgent
QUESTION: Why is defaultAgent assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused destructured prop sessionProp in Scenario
QUESTION: Why is sessionProp extracted but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable session in Scenario
QUESTION: Why is session assigned but never used?

FILE: packages/test-agent/src/index.ts
PATTERN: dead variable
CONCERN: Unused variable declaredIn in Scenario
QUESTION: Why is declaredIn assigned but never used?

taras added 2 commits August 2, 2026 10:38
TV13 pins the regression the scoped() removal fixes: a scenario in the body sees
the session <TestAgent> installed. TV14 is TC1's analog — a settled diagnostic
renders inline beside a healthy scenario and the rest of the body still runs,
where content() would have replaced the invocation's whole output with it. Both
verified red against a restored scoped() wrapper, with the mutation confirmed to
have applied.
TV14 also pins the empty-string src: the schema accepts "" as a string, so the
component's own check still owns that wording and did not move.
Parity audit, by grep rather than assertion: no existing test asserted any of
the three diagnostics that moved to schema rejections — the agent-prop literal
checks on both components and Scenario's missing-src case. That is why the
suites passed unchanged, not because nothing moved. The new wording source is
the engine's PropValidationError.
Spec states what registration makes true: both are non-reserved registered
defaults a repository component replaces, the dotted name resolves to
components/TestAgent/Scenario.md, and props are schema-validated. The controller
now stops with the invocation rather than a nested scope.
TV15 pins that a repository component overrides either registered name,
including the dotted one at components/TestAgent/Scenario.md. The fixtures were
invisible because the default component dirs resolve against the process cwd,
not the temp project — the testing package's harness gets away with it through
useStubFs. runDoc now threads componentDirs, relative to the fixture directory.
Mutation attestation, each confirmed applied before the run and reverted after:
boundary() keys on the nearest eval scope instead of testScope
→ TV8, TV10, TV11 red. The multi-prompt tests, which is exactly the
signature of the regression this key exists to prevent.
installPromptFailurePolicy made unconditional
→ nothing red. A coverage gap, reported rather than papered over.
The gap is left open deliberately. The obvious pin — a prompt failing outside a
<Test> rendering a diagnostic while the document carries on — does not hold:
such a prompt renders nothing at all, so the assertion has no observable to
stand on. That behavior is worth understanding before a test is written against
it, and guessing at one that passes for the wrong reason would be worse than
naming the gap.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

// NOT wrapped in scoped(): content projected by tryContent() anchors to the
// invocation, not to a child frame, so anything installed inside a scoped()
// here would be invisible to the body. The invocation is already the bound
// this region needs — it is dismantled with the component.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// this region needs — it is dismantled with the component.


// Non-reserved defaults: a repository component of either name is chosen
// ahead of these. The dotted name addresses a subdirectory, so the override
// for the second is components/TestAgent/Scenario.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// for the second is components/TestAgent/Scenario.md.

@taras
taras merged commit 3d00dcb into mainAug 2, 2026
9 checks passed
@taras
taras deleted the feat/test-agent-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras