From 3f74ab93bc056cace6ee7989068bfe9be05ed26b Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Fri, 10 Apr 2026 13:25:11 -0400 Subject: [PATCH 1/5] Make ValidationPipeline issue-scoped so TestRun cards correlate to specific issues Replace `validator: Validator` with `createValidator: (issueId: string) => Validator` in IssueLoopConfig. The outer loop now creates a fresh validator per issue, passing the issue ID so artifacts like TestRun cards are scoped per-issue instead of shared. Closes CS-10723 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/smoke-tests/issue-loop-smoke.ts | 42 ++++--- packages/software-factory/src/issue-loop.ts | 13 ++- .../software-factory/tests/issue-loop.test.ts | 105 ++++++++++++++---- 3 files changed, 111 insertions(+), 49 deletions(-) diff --git a/packages/software-factory/scripts/smoke-tests/issue-loop-smoke.ts b/packages/software-factory/scripts/smoke-tests/issue-loop-smoke.ts index 79dcb63bf4..bbdb5f11df 100644 --- a/packages/software-factory/scripts/smoke-tests/issue-loop-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/issue-loop-smoke.ts @@ -271,7 +271,7 @@ async function scenarioSingleIssue(): Promise { contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: new MockValidator([makePassingValidation()]), + createValidator: () => new MockValidator([makePassingValidation()]), targetRealmUrl: 'https://example.test/target/', }); @@ -328,10 +328,7 @@ async function scenarioDependencyCascade(): Promise { contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: new MockValidator([ - makePassingValidation(), - makePassingValidation(), - ]), + createValidator: () => new MockValidator([makePassingValidation()]), targetRealmUrl: 'https://example.test/target/', }); @@ -402,11 +399,7 @@ async function scenarioPriorityOrdering(): Promise { contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: new MockValidator([ - makePassingValidation(), - makePassingValidation(), - makePassingValidation(), - ]), + createValidator: () => new MockValidator([makePassingValidation()]), targetRealmUrl: 'https://example.test/target/', }); @@ -461,7 +454,7 @@ async function scenarioMaxIterations(): Promise { contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: new MockValidator(validations), + createValidator: () => new MockValidator(validations), targetRealmUrl: 'https://example.test/target/', maxIterationsPerIssue: 3, }); @@ -507,7 +500,7 @@ async function scenarioBlockedIssue(): Promise { contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: new MockValidator([makePassingValidation()]), + createValidator: () => new MockValidator([makePassingValidation()]), targetRealmUrl: 'https://example.test/target/', }); @@ -535,7 +528,7 @@ async function scenarioEmptyProject(): Promise { contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: new NoOpValidator(), + createValidator: () => new NoOpValidator(), targetRealmUrl: 'https://example.test/target/', }); @@ -572,21 +565,23 @@ async function scenarioValidationPipeline(): Promise { store, ); - // Use a real ValidationPipeline with all NoOp steps (no server needed) - let pipeline = new ValidationPipeline([ - new NoOpStepRunner('parse'), - new NoOpStepRunner('lint'), - new NoOpStepRunner('evaluate'), - new NoOpStepRunner('instantiate'), - new NoOpStepRunner('test'), - ]); + // Use a real ValidationPipeline with all NoOp steps (no server needed). + // The factory creates a fresh pipeline per issue, as in production. + let createPipeline = () => + new ValidationPipeline([ + new NoOpStepRunner('parse'), + new NoOpStepRunner('lint'), + new NoOpStepRunner('evaluate'), + new NoOpStepRunner('instantiate'), + new NoOpStepRunner('test'), + ]); let result = await runIssueLoop({ agent, contextBuilder: new StubContextBuilder(), tools: TOOLS, issueStore: store, - validator: pipeline, + createValidator: createPipeline, targetRealmUrl: 'https://example.test/target/', }); @@ -604,8 +599,9 @@ async function scenarioValidationPipeline(): Promise { // Verify formatForContext works let lastValidation = result.issueResults[0]?.lastValidation; + let pipelineForFormat = createPipeline(); let formatted = lastValidation - ? pipeline.formatForContext(lastValidation) + ? pipelineForFormat.formatForContext(lastValidation) : ''; check( 'formatForContext reports all passed', diff --git a/packages/software-factory/src/issue-loop.ts b/packages/software-factory/src/issue-loop.ts index 7052bfc0fb..426e505565 100644 --- a/packages/software-factory/src/issue-loop.ts +++ b/packages/software-factory/src/issue-loop.ts @@ -91,7 +91,12 @@ export interface IssueLoopConfig { contextBuilder: IssueContextBuilderLike; tools: FactoryTool[]; issueStore: IssueStore; - validator: Validator; + /** + * Factory that creates a fresh Validator for each issue. + * Receives the issue ID so the validator can scope artifacts (e.g. TestRun + * slugs) to the specific issue being validated. + */ + createValidator: (issueId: string) => Validator; targetRealmUrl: string; briefUrl?: string; /** Maximum inner-loop iterations per issue. Default: 5. */ @@ -199,7 +204,7 @@ export async function runIssueLoop( contextBuilder, tools, issueStore, - validator, + createValidator, targetRealmUrl, briefUrl, maxIterationsPerIssue = DEFAULT_MAX_ITERATIONS_PER_ISSUE, @@ -254,6 +259,10 @@ export async function runIssueLoop( `Outer cycle ${outerCycles}: picked issue ${issueSummaryLabel(issue)} (status=${issue.status}, priority=${issue.priority})`, ); + // Create a fresh validator scoped to this issue so that artifacts + // (e.g. TestRun cards) are named per-issue rather than shared. + let validator = createValidator(issue.id); + // ----------------------------------------------------------------------- // Inner loop: iterate on a single issue with validation // ----------------------------------------------------------------------- diff --git a/packages/software-factory/tests/issue-loop.test.ts b/packages/software-factory/tests/issue-loop.test.ts index b5f1f72336..59a406907d 100644 --- a/packages/software-factory/tests/issue-loop.test.ts +++ b/packages/software-factory/tests/issue-loop.test.ts @@ -238,7 +238,7 @@ function makeLoopConfig( return { contextBuilder: new StubIssueContextBuilder(), tools: DEFAULT_TOOLS, - validator: new MockValidator([makePassingValidation()]), + createValidator: () => new MockValidator([makePassingValidation()]), targetRealmUrl: 'https://example.test/target/', maxIterationsPerIssue: 5, maxOuterCycles: 50, @@ -272,7 +272,7 @@ module('issue-loop > happy path', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator([makePassingValidation()]), + createValidator: () => new MockValidator([makePassingValidation()]), }), ); @@ -324,10 +324,7 @@ module('issue-loop > multiple issues', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator([ - makePassingValidation(), - makePassingValidation(), - ]), + createValidator: () => new MockValidator([makePassingValidation()]), }), ); @@ -379,10 +376,8 @@ module('issue-loop > validation failure', function () { agent, issueStore: store, contextBuilder, - validator: new MockValidator([ - makeFailingValidation(), - makePassingValidation(), - ]), + createValidator: () => + new MockValidator([makeFailingValidation(), makePassingValidation()]), }), ); @@ -436,10 +431,7 @@ module('issue-loop > blocked issue', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator([ - makePassingValidation(), - makePassingValidation(), - ]), + createValidator: () => new MockValidator([makePassingValidation()]), }), ); @@ -484,7 +476,7 @@ module('issue-loop > max inner iterations', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator(validations), + createValidator: () => new MockValidator(validations), maxIterationsPerIssue: 3, }), ); @@ -524,7 +516,7 @@ module('issue-loop > max inner iterations', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator(validations), + createValidator: () => new MockValidator(validations), maxIterationsPerIssue: 3, }), ); @@ -557,7 +549,7 @@ module('issue-loop > max inner iterations', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator(validations), + createValidator: () => new MockValidator(validations), maxIterationsPerIssue: 2, }), ); @@ -653,7 +645,7 @@ module('issue-loop > NoOpValidator', function () { makeLoopConfig({ agent, issueStore: store, - validator: new NoOpValidator(), + createValidator: () => new NoOpValidator(), briefUrl: 'https://example.test/brief/', }), ); @@ -703,7 +695,8 @@ module('issue-loop > context threading', function () { agent, issueStore: store, contextBuilder, - validator: new MockValidator([failValidation, makePassingValidation()]), + createValidator: () => + new MockValidator([failValidation, makePassingValidation()]), }), ); @@ -749,7 +742,7 @@ module('issue-loop > brief URL threading', function () { agent, issueStore: store, contextBuilder, - validator: new MockValidator([makePassingValidation()]), + createValidator: () => new MockValidator([makePassingValidation()]), briefUrl: 'https://example.test/brief/', }), ); @@ -822,10 +815,7 @@ module('issue-loop > new issues mid-loop', function () { makeLoopConfig({ agent, issueStore: store, - validator: new MockValidator([ - makePassingValidation(), - makePassingValidation(), - ]), + createValidator: () => new MockValidator([makePassingValidation()]), }), ); @@ -835,3 +825,70 @@ module('issue-loop > new issues mid-loop', function () { assert.strictEqual(result.issueResults[1].issueId, 'new-1'); }); }); + +// --------------------------------------------------------------------------- +// 12. createValidator receives issue ID +// --------------------------------------------------------------------------- + +module('issue-loop > createValidator receives issue ID', function () { + test('createValidator is called with the current issue ID', async function (assert) { + let store = new MockIssueStore([ + makeIssue({ + id: 'Issues/sticky-note-define-core', + status: 'backlog', + priority: 'high', + order: 1, + }), + makeIssue({ + id: 'Issues/sticky-note-catalog-spec', + status: 'backlog', + priority: 'medium', + order: 2, + }), + ]); + + let agent = new MockLoopAgent( + [ + { + toolCalls: [ + { tool: 'write_file', args: { path: 'a.gts', content: '' } }, + ], + updateIssue: { + id: 'Issues/sticky-note-define-core', + status: 'done', + }, + }, + { + toolCalls: [ + { tool: 'write_file', args: { path: 'b.gts', content: '' } }, + ], + updateIssue: { + id: 'Issues/sticky-note-catalog-spec', + status: 'done', + }, + }, + ], + store, + ); + + let receivedIssueIds: string[] = []; + + let result = await runIssueLoop( + makeLoopConfig({ + agent, + issueStore: store, + createValidator: (issueId: string) => { + receivedIssueIds.push(issueId); + return new MockValidator([makePassingValidation()]); + }, + }), + ); + + assert.strictEqual(result.outcome, 'all_issues_done'); + assert.deepEqual( + receivedIssueIds, + ['Issues/sticky-note-define-core', 'Issues/sticky-note-catalog-spec'], + 'createValidator received the correct issue IDs in order', + ); + }); +}); From add5cf208917096d65000b184654b6895b2a3212 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Fri, 10 Apr 2026 13:40:06 -0400 Subject: [PATCH 2/5] Thread issueURL to TestRun, show issue in fitted view, reset seq per slug - Pass issueId as issueURL from TestValidationStep to executeTestRunFn so the TestRun card gets a proper linksTo relationship to the driving issue - Add issue slug to the QUnit isolated template for observability - Scope getNextSequenceNumber by slug (using _mtimes filename listing) so each issue's test runs start from sequence 1 independently - Show issue.summary in the TestRun fitted/embedded view when linked - Merge upstream main (includes test-results.gts module grouping changes) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../software-factory/realm/test-results.gts | 10 ++++ .../src/test-run-execution.ts | 60 ++++++++++++------- .../src/validators/test-step.ts | 7 +++ .../tests/factory-test-realm.test.ts | 20 ++++++- 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/packages/software-factory/realm/test-results.gts b/packages/software-factory/realm/test-results.gts index 4e05ca8ad6..ec0e1c7acc 100644 --- a/packages/software-factory/realm/test-results.gts +++ b/packages/software-factory/realm/test-results.gts @@ -234,6 +234,9 @@ export class TestRun extends CardDef { <strong>#{{@model.sequenceNumber}}</strong> <span class='status status-{{@model.status}}'>{{@model.status}}</span> </div> + {{#if @model.issue}} + <div class='issue-name'>{{@model.issue.summary}}</div> + {{/if}} <div class='counts'> {{@model.passedCount}}/{{this.total}} passed @@ -281,6 +284,13 @@ export class TestRun extends CardDef { color: var(--boxel-blue, #2563eb); background: #eff6ff; } + .issue-name { + font-size: 0.8rem; + color: var(--muted-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .counts { font-size: 0.85rem; color: var(--muted-foreground); diff --git a/packages/software-factory/src/test-run-execution.ts b/packages/software-factory/src/test-run-execution.ts index ebc22f5427..7e11aec05e 100644 --- a/packages/software-factory/src/test-run-execution.ts +++ b/packages/software-factory/src/test-run-execution.ts @@ -6,7 +6,11 @@ import { logger } from './logger'; import { chromium } from '@playwright/test'; -import { ensureTrailingSlash, searchRealm } from './realm-operations'; +import { + ensureTrailingSlash, + fetchRealmFilenames, + searchRealm, +} from './realm-operations'; import { createTestRun, completeTestRun } from './test-run-cards'; import { parseQunitResults } from './test-run-parsing'; import type { @@ -58,6 +62,7 @@ export async function resolveTestRun( } let sequenceNumber = await getNextSequenceNumber( + options.slug, realmOptions, options.lastSequenceNumber, ); @@ -142,29 +147,41 @@ async function findResumableTestRun( }; } +/** + * Get the next sequence number for a given slug by inspecting existing + * TestRun filenames in the realm. Each slug (issue) gets its own + * independent sequence starting from 1. + */ async function getNextSequenceNumber( + slug: string, options: TestRunRealmOptions, minSequenceNumber = 0, ): Promise<number> { - let result = await searchRealm( - options.targetRealmUrl, - { - filter: { - on: { module: options.testResultsModuleUrl, name: 'TestRun' }, - }, - sort: [{ by: 'sequenceNumber', direction: 'desc' }], - page: { size: 1 }, - }, - { authorization: options.authorization, fetch: options.fetch }, - ); + let result = await fetchRealmFilenames(options.targetRealmUrl, { + authorization: options.authorization, + fetch: options.fetch, + }); + + if (result.error) { + log.warn( + `Failed to fetch filenames for sequence number — falling back to minSequenceNumber: ${result.error}`, + ); + return minSequenceNumber + 1; + } + + let prefix = `Test Runs/${slug}-`; + let maxSeq = 0; + for (let filename of result.filenames) { + if (filename.startsWith(prefix) && filename.endsWith('.json')) { + let seqStr = filename.slice(prefix.length, -'.json'.length); + let seq = parseInt(seqStr, 10); + if (!isNaN(seq) && seq > maxSeq) { + maxSeq = seq; + } + } + } - let latest = result?.ok - ? (result.data?.[0] as - | { attributes?: { sequenceNumber?: number } } - | undefined) - : undefined; - let fromIndex = latest?.attributes?.sequenceNumber ?? 0; - return Math.max(fromIndex, minSequenceNumber) + 1; + return Math.max(maxSeq, minSequenceNumber) + 1; } // --------------------------------------------------------------------------- @@ -190,6 +207,8 @@ function buildQunitTestPageHtml(opts: { targetRealmUrl: string; /** Browser-accessible URL of the realm server (compat proxy) */ realmProxyUrl: string; + /** Optional slug identifying the issue under test — shown in the page title. */ + slug?: string; }): string { let host = opts.assetServerUrl.replace(/\/$/, ''); // Ember config URLs must use the browser-accessible realm proxy, @@ -268,7 +287,7 @@ function buildQunitTestPageHtml(opts: { <head> <meta charset="utf-8"> ${metaTags.join('\n ')} - <title>Software Factory Card Tests + Software Factory Card Tests${opts.slug ? ` — ${opts.slug}` : ''} ${linkTags.join('\n ')} @@ -466,6 +485,7 @@ export async function executeTestRunFromRealm( hostDistDir, targetRealmUrl: options.targetRealmUrl, realmProxyUrl: options.hostAppUrl, + slug: options.slug, }); setHtml(html); diff --git a/packages/software-factory/src/validators/test-step.ts b/packages/software-factory/src/validators/test-step.ts index 9ef632af18..d370d15f70 100644 --- a/packages/software-factory/src/validators/test-step.ts +++ b/packages/software-factory/src/validators/test-step.ts @@ -138,6 +138,12 @@ export class TestValidationStep implements ValidationStepRunner { ? deriveIssueSlug(this.config.issueId) : 'validation'; + // Build the issue card URL for the TestRun → Issue linksTo relationship. + // issueId is a realm-relative path like "Issues/sticky-note-define-core". + let issueURL = this.config.issueId + ? new URL(this.config.issueId, targetRealmUrl).href + : undefined; + handle = await this.executeTestRunFn({ targetRealmUrl, testResultsModuleUrl: this.config.testResultsModuleUrl, @@ -149,6 +155,7 @@ export class TestValidationStep implements ValidationStepRunner { hostAppUrl: this.config.hostAppUrl, forceNew: true, lastSequenceNumber: this.lastSequenceNumber, + issueURL, }); if (handle.sequenceNumber != null) { diff --git a/packages/software-factory/tests/factory-test-realm.test.ts b/packages/software-factory/tests/factory-test-realm.test.ts index e311ccc395..e14f9216ef 100644 --- a/packages/software-factory/tests/factory-test-realm.test.ts +++ b/packages/software-factory/tests/factory-test-realm.test.ts @@ -492,7 +492,25 @@ module('factory-test-realm > resolveTestRun', function () { let urlStr = String(url); let method = init?.method ?? 'GET'; - // Search endpoint + // _mtimes endpoint (used by getNextSequenceNumber to list filenames) + if (urlStr.includes('_mtimes') && method === 'GET') { + let mtimes: Record = {}; + for (let tr of testRuns) { + let fullUrl = `https://realms.example.test/user/personal/${tr.id}.json`; + mtimes[fullUrl] = Date.now(); + } + return new Response( + JSON.stringify({ + data: { attributes: { mtimes } }, + }), + { + status: 200, + headers: { 'Content-Type': SupportedMimeType.JSONAPI }, + }, + ); + } + + // Search endpoint (used by findResumableTestRun) if (urlStr.includes('_search') && method === 'QUERY') { return new Response( JSON.stringify({ From 3aa762b2bd2b0455597576322b9d9b9a171d010e Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Fri, 10 Apr 2026 14:04:49 -0400 Subject: [PATCH 3/5] Use search API instead of _mtimes for slug-scoped sequence numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace fetchRealmFilenames (O(all files)) with searchRealm filtered to TestRun cards. Filter by card ID matching the slug prefix to get per-issue sequence numbers. More targeted — only fetches TestRun cards, not the entire realm file listing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/test-run-execution.ts | 48 +++++++++++-------- .../tests/factory-test-realm.test.ts | 20 +------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/software-factory/src/test-run-execution.ts b/packages/software-factory/src/test-run-execution.ts index 7e11aec05e..306d5802e3 100644 --- a/packages/software-factory/src/test-run-execution.ts +++ b/packages/software-factory/src/test-run-execution.ts @@ -6,11 +6,7 @@ import { logger } from './logger'; import { chromium } from '@playwright/test'; -import { - ensureTrailingSlash, - fetchRealmFilenames, - searchRealm, -} from './realm-operations'; +import { ensureTrailingSlash, searchRealm } from './realm-operations'; import { createTestRun, completeTestRun } from './test-run-cards'; import { parseQunitResults } from './test-run-parsing'; import type { @@ -148,34 +144,44 @@ async function findResumableTestRun( } /** - * Get the next sequence number for a given slug by inspecting existing - * TestRun filenames in the realm. Each slug (issue) gets its own - * independent sequence starting from 1. + * Get the next sequence number for a given slug by searching existing + * TestRun cards in the realm and filtering by card ID. Each slug (issue) + * gets its own independent sequence starting from 1. */ async function getNextSequenceNumber( slug: string, options: TestRunRealmOptions, minSequenceNumber = 0, ): Promise { - let result = await fetchRealmFilenames(options.targetRealmUrl, { - authorization: options.authorization, - fetch: options.fetch, - }); + let result = await searchRealm( + options.targetRealmUrl, + { + filter: { + on: { module: options.testResultsModuleUrl, name: 'TestRun' }, + }, + sort: [{ by: 'sequenceNumber', direction: 'desc' }], + }, + { authorization: options.authorization, fetch: options.fetch }, + ); - if (result.error) { - log.warn( - `Failed to fetch filenames for sequence number — falling back to minSequenceNumber: ${result.error}`, - ); + if (!result?.ok || !result.data) { return minSequenceNumber + 1; } + let targetRealmUrl = ensureTrailingSlash(options.targetRealmUrl); let prefix = `Test Runs/${slug}-`; let maxSeq = 0; - for (let filename of result.filenames) { - if (filename.startsWith(prefix) && filename.endsWith('.json')) { - let seqStr = filename.slice(prefix.length, -'.json'.length); - let seq = parseInt(seqStr, 10); - if (!isNaN(seq) && seq > maxSeq) { + + for (let card of result.data) { + let cardId = (card as { id?: string }).id ?? ''; + let relativePath = cardId.startsWith(targetRealmUrl) + ? cardId.slice(targetRealmUrl.length) + : cardId; + if (relativePath.startsWith(prefix)) { + let attrs = (card as { attributes?: { sequenceNumber?: number } }) + .attributes; + let seq = attrs?.sequenceNumber ?? 0; + if (seq > maxSeq) { maxSeq = seq; } } diff --git a/packages/software-factory/tests/factory-test-realm.test.ts b/packages/software-factory/tests/factory-test-realm.test.ts index e14f9216ef..1fe682b96e 100644 --- a/packages/software-factory/tests/factory-test-realm.test.ts +++ b/packages/software-factory/tests/factory-test-realm.test.ts @@ -492,25 +492,7 @@ module('factory-test-realm > resolveTestRun', function () { let urlStr = String(url); let method = init?.method ?? 'GET'; - // _mtimes endpoint (used by getNextSequenceNumber to list filenames) - if (urlStr.includes('_mtimes') && method === 'GET') { - let mtimes: Record = {}; - for (let tr of testRuns) { - let fullUrl = `https://realms.example.test/user/personal/${tr.id}.json`; - mtimes[fullUrl] = Date.now(); - } - return new Response( - JSON.stringify({ - data: { attributes: { mtimes } }, - }), - { - status: 200, - headers: { 'Content-Type': SupportedMimeType.JSONAPI }, - }, - ); - } - - // Search endpoint (used by findResumableTestRun) + // Search endpoint (used by findResumableTestRun and getNextSequenceNumber) if (urlStr.includes('_search') && method === 'QUERY') { return new Response( JSON.stringify({ From 9c88a5bb1c0329b8408357dece5caf15c72b8c46 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Fri, 10 Apr 2026 14:09:07 -0400 Subject: [PATCH 4/5] Fix factory-target-realm Playwright test to match actual summary shape The test expected `summary.seedIssue.seedIssueId` but the actual entrypoint output uses `summary.bootstrap.activeIssue.id`. Updated assertions to match the real FactoryEntrypointSummary structure: - activeIssue.id = 'Issues/sticky-note-define-core' (derived from brief title) - issueType = 'implementation' (not 'bootstrap') - status = 'in_progress' (bootstrap patches the first issue) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/factory-target-realm.spec.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/software-factory/tests/factory-target-realm.spec.ts b/packages/software-factory/tests/factory-target-realm.spec.ts index f20a031486..0faa2a3ff9 100644 --- a/packages/software-factory/tests/factory-target-realm.spec.ts +++ b/packages/software-factory/tests/factory-target-realm.spec.ts @@ -103,18 +103,21 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- let summary = JSON.parse(result.stdout) as { command: string; targetRealm: { url: string; ownerUsername: string }; - seedIssue: { - seedIssueId: string; - seedIssueStatus: string; + bootstrap: { + projectId: string; + issueIds: string[]; + activeIssue: { id: string; status: string }; }; }; expect(summary.command).toBe('factory:go'); expect(summary.targetRealm.ownerUsername).toBe(targetUsername); - expect(summary.seedIssue.seedIssueId).toBe('Issues/bootstrap-seed'); - expect(summary.seedIssue.seedIssueStatus).toBe('created'); + expect(summary.bootstrap.activeIssue.id).toBe( + 'Issues/sticky-note-define-core', + ); + expect(summary.bootstrap.activeIssue.status).toBe('created'); - // Verify the seed issue actually exists in the newly created target realm + // Verify the active issue actually exists in the newly created target realm // by authenticating as the target user who owns the realm let targetRealmToken = await getRealmToken( matrixURL, @@ -123,17 +126,19 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- summary.targetRealm.url, ); - let seedIssueUrl = new URL('Issues/bootstrap-seed', summary.targetRealm.url) - .href; - let seedIssueResponse = await fetch(seedIssueUrl, { + let activeIssueUrl = new URL( + summary.bootstrap.activeIssue.id, + summary.targetRealm.url, + ).href; + let issueResponse = await fetch(activeIssueUrl, { headers: { Accept: SupportedMimeType.CardSource, Authorization: targetRealmToken, }, }); - expect(seedIssueResponse.ok).toBe(true); - let issueJson = (await seedIssueResponse.json()) as { + expect(issueResponse.ok).toBe(true); + let issueJson = (await issueResponse.json()) as { data: { attributes: { issueType: string; @@ -142,11 +147,9 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- }; }; }; - expect(issueJson.data.attributes.issueType).toBe('bootstrap'); - expect(issueJson.data.attributes.status).toBe('backlog'); - expect(issueJson.data.attributes.summary).toContain( - 'Process brief and create project artifacts', - ); + expect(issueJson.data.attributes.issueType).toBe('implementation'); + expect(issueJson.data.attributes.status).toBe('in_progress'); + expect(issueJson.data.attributes.summary).toContain('Sticky Note'); } finally { await new Promise((r, reject) => briefServer.close((err) => (err ? reject(err) : r())), From e7fd709fa63a3eadcfec14360199b3cee9b75495 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Fri, 10 Apr 2026 14:35:14 -0400 Subject: [PATCH 5/5] Fix issueType assertion: bootstrap creates 'feature' issues, not 'implementation' Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/software-factory/tests/factory-target-realm.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/software-factory/tests/factory-target-realm.spec.ts b/packages/software-factory/tests/factory-target-realm.spec.ts index 0faa2a3ff9..340cdcff74 100644 --- a/packages/software-factory/tests/factory-target-realm.spec.ts +++ b/packages/software-factory/tests/factory-target-realm.spec.ts @@ -147,7 +147,7 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- }; }; }; - expect(issueJson.data.attributes.issueType).toBe('implementation'); + expect(issueJson.data.attributes.issueType).toBe('feature'); expect(issueJson.data.attributes.status).toBe('in_progress'); expect(issueJson.data.attributes.summary).toContain('Sticky Note'); } finally {